Files
jackwener__opencli/clis/v2ex/daily.ts
jakevin 4fe9a73ebc refactor: migrate adapter imports to package exports (#795)
* refactor: migrate adapter imports to package exports

Replace all relative imports (../../src/registry.js, ../../browser/cdp.js, etc.)
with package exports (@jackwener/opencli/registry, @jackwener/opencli/errors, etc.)
across all 484 adapter files.

This decouples adapter import resolution from directory structure:
- User CLIs in ~/.opencli/clis/ resolve via node_modules symlink
- Internal adapters resolve via Node.js self-referencing
- No more shim files needed for import resolution

Changes:
- package.json: add sub-path exports for all public modules
- clis/**: replace relative imports with @jackwener/opencli/...
- discovery.ts: simplify ensureUserCliCompatShims to symlink-only
- registry-api.ts: export CommandArgs type
- Remove root-level shim directories (browser/, download/, pipeline/)
- Remove shim entries from tsconfig.json include and package.json files

* test: add regression tests for package exports

Prevents regressions like #788/#791 by:
1. Scanning all adapter files for forbidden relative imports
   (../../src/, ../../browser/, etc.) — fails if any remain
2. Verifying every package.json export maps to an existing source file

18 new test cases.

* fix: use junction on Windows + broaden test patterns

- discovery.ts: use 'junction' symlink type on Windows (no admin required)
- package-exports.test.ts: generalize forbidden patterns to catch any
  depth of ../ traversal (not just ../../ and ../../../)

* fix: update stale vi.mock/importActual paths in adapter tests

Test files still used old relative paths for vi.mock() and
vi.importActual() calls. Updated 5 test files to use package exports.
Also broadened regression test patterns to catch mock/importActual paths.

* fix: use rm instead of unlink for symlink cleanup, add warn on failure

Addresses review feedback from Astro-Han:
- rm() handles both symlinks and stale directories (unlink fails on dirs)
- Log a warning when symlink creation fails instead of silent catch

* docs: update import examples to use package exports

Update all documentation, contributing guides, and skills to use
@jackwener/opencli/registry instead of ../../src/registry.js.

Without this, users following the docs would write adapters with
broken imports since the old shim files are no longer created.
2026-04-05 16:02:17 +08:00

107 lines
3.7 KiB
TypeScript

/**
* V2EX Daily Check-in adapter.
*/
import { CommandExecutionError } from '@jackwener/opencli/errors';
import { cli, Strategy } from '@jackwener/opencli/registry';
import type { IPage } from '@jackwener/opencli/types';
cli({
site: 'v2ex',
name: 'daily',
description: 'V2EX 每日签到并领取铜币',
domain: 'www.v2ex.com',
strategy: Strategy.COOKIE,
browser: true,
args: [],
columns: ['status', 'message'],
func: async (page: IPage | null) => {
if (!page) throw new CommandExecutionError('Browser page required');
if (process.env.OPENCLI_VERBOSE) {
console.error('[opencli:v2ex] Navigating to /mission/daily');
}
await page.goto('https://www.v2ex.com/mission/daily');
// Cloudflare challenge bypass wait
for (let i = 0; i < 5; i++) {
await new Promise(r => setTimeout(r, 1500));
const title = await page.evaluate(`() => document.title`);
if (!title?.includes('Just a moment')) break;
if (process.env.OPENCLI_VERBOSE) console.error('[opencli:v2ex] Waiting for Cloudflare...');
}
// Evaluate DOM to find if we need to check in
const checkResult = await page.evaluate(`
async () => {
const btn = document.querySelector('input.super.normal.button');
if (!btn || !btn.value.includes('领取')) {
return { claimed: true, message: '今日奖励已发/无需领取' };
}
const onclick = btn.getAttribute('onclick');
if (onclick) {
const match = onclick.match(/once=(\\d+)/);
if (match) {
return { claimed: false, once: match[1], message: btn.value };
}
}
return {
claimed: false,
error: '找到了按钮,但未能提取 once token',
debug_title: document.title,
debug_body: document.body.innerText.substring(0, 200).replace(/\\n/g, ' ')
};
}
`);
if (checkResult.error) {
if (process.env.OPENCLI_VERBOSE) {
console.error(`[opencli:v2ex:debug] Page Title: ${checkResult.debug_title}`);
console.error(`[opencli:v2ex:debug] Page Body: ${checkResult.debug_body}`);
}
throw new CommandExecutionError(checkResult.error);
}
if (checkResult.claimed) {
return [{ status: '✅ 已签到', message: checkResult.message }];
}
// Perform check in
if (process.env.OPENCLI_VERBOSE) {
console.error(`[opencli:v2ex] Found check-in token: once=${checkResult.once}. Checking in...`);
}
await page.goto(`https://www.v2ex.com/mission/daily/redeem?once=${checkResult.once}`);
await new Promise(resolve => setTimeout(resolve, 3000)); // wait longer for redirect
// Verify result
const verifyResult = await page.evaluate(`
async () => {
const btn = document.querySelector('input.super.normal.button');
if (!btn || !btn.value.includes('领取')) {
// fetch balance to show user
let balance = '';
const balanceLink = document.querySelector('a.balance_area');
if (balanceLink) {
balance = Array.from(balanceLink.childNodes)
.filter(n => n.nodeType === 3)
.map(n => n.textContent?.trim())
.join(' ')
.trim();
}
return { success: true, balance };
}
return { success: false };
}
`);
if (verifyResult.success) {
return [{ status: '🎉 签到成功', message: `当前余额: ${verifyResult.balance || '未知'}` }];
} else {
return [{ status: '❌ 签到失败', message: '未能确认签到结果,请手动检查' }];
}
},
});