trustasia-ones-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +150 -0
  2. package/dist/auth/browser-auth.d.ts +64 -0
  3. package/dist/auth/browser-auth.js +327 -0
  4. package/dist/auth/browser-auth.js.map +1 -0
  5. package/dist/auth/challenge-detector.d.ts +22 -0
  6. package/dist/auth/challenge-detector.js +75 -0
  7. package/dist/auth/challenge-detector.js.map +1 -0
  8. package/dist/auth/chromium-installer.d.ts +11 -0
  9. package/dist/auth/chromium-installer.js +72 -0
  10. package/dist/auth/chromium-installer.js.map +1 -0
  11. package/dist/auth/config-redactor.d.ts +9 -0
  12. package/dist/auth/config-redactor.js +41 -0
  13. package/dist/auth/config-redactor.js.map +1 -0
  14. package/dist/auth/credential-provider.d.ts +41 -0
  15. package/dist/auth/credential-provider.js +230 -0
  16. package/dist/auth/credential-provider.js.map +1 -0
  17. package/dist/auth/credential-store.d.ts +15 -0
  18. package/dist/auth/credential-store.js +160 -0
  19. package/dist/auth/credential-store.js.map +1 -0
  20. package/dist/auth/dingtalk-login.d.ts +72 -0
  21. package/dist/auth/dingtalk-login.js +136 -0
  22. package/dist/auth/dingtalk-login.js.map +1 -0
  23. package/dist/auth/errors.d.ts +51 -0
  24. package/dist/auth/errors.js +105 -0
  25. package/dist/auth/errors.js.map +1 -0
  26. package/dist/auth/extract.d.ts +6 -0
  27. package/dist/auth/extract.js +60 -0
  28. package/dist/auth/extract.js.map +1 -0
  29. package/dist/auth/index.d.ts +14 -0
  30. package/dist/auth/index.js +13 -0
  31. package/dist/auth/index.js.map +1 -0
  32. package/dist/auth/paths.d.ts +19 -0
  33. package/dist/auth/paths.js +27 -0
  34. package/dist/auth/paths.js.map +1 -0
  35. package/dist/auth/phone-password-provider.d.ts +17 -0
  36. package/dist/auth/phone-password-provider.js +27 -0
  37. package/dist/auth/phone-password-provider.js.map +1 -0
  38. package/dist/auth/types.d.ts +42 -0
  39. package/dist/auth/types.js +4 -0
  40. package/dist/auth/types.js.map +1 -0
  41. package/dist/cli/argv.d.ts +20 -0
  42. package/dist/cli/argv.js +46 -0
  43. package/dist/cli/argv.js.map +1 -0
  44. package/dist/cli/claude-desktop-config.d.ts +16 -0
  45. package/dist/cli/claude-desktop-config.js +125 -0
  46. package/dist/cli/claude-desktop-config.js.map +1 -0
  47. package/dist/cli/index.d.ts +3 -0
  48. package/dist/cli/index.js +108 -0
  49. package/dist/cli/index.js.map +1 -0
  50. package/dist/cli/login.d.ts +4 -0
  51. package/dist/cli/login.js +67 -0
  52. package/dist/cli/login.js.map +1 -0
  53. package/dist/cli/platform-paths.d.ts +7 -0
  54. package/dist/cli/platform-paths.js +33 -0
  55. package/dist/cli/platform-paths.js.map +1 -0
  56. package/dist/cli/prompt.d.ts +27 -0
  57. package/dist/cli/prompt.js +132 -0
  58. package/dist/cli/prompt.js.map +1 -0
  59. package/dist/cli/setup.d.ts +15 -0
  60. package/dist/cli/setup.js +126 -0
  61. package/dist/cli/setup.js.map +1 -0
  62. package/dist/cli/status.d.ts +13 -0
  63. package/dist/cli/status.js +116 -0
  64. package/dist/cli/status.js.map +1 -0
  65. package/dist/client/graphql.d.ts +5 -0
  66. package/dist/client/graphql.js +72 -0
  67. package/dist/client/graphql.js.map +1 -0
  68. package/dist/client/ones-client.d.ts +78 -0
  69. package/dist/client/ones-client.js +236 -0
  70. package/dist/client/ones-client.js.map +1 -0
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.js +114 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/register-tools.d.ts +13 -0
  75. package/dist/register-tools.js +123 -0
  76. package/dist/register-tools.js.map +1 -0
  77. package/dist/schemas/issue.d.ts +111 -0
  78. package/dist/schemas/issue.js +39 -0
  79. package/dist/schemas/issue.js.map +1 -0
  80. package/dist/schemas/wiki.d.ts +59 -0
  81. package/dist/schemas/wiki.js +24 -0
  82. package/dist/schemas/wiki.js.map +1 -0
  83. package/dist/server.d.ts +2 -0
  84. package/dist/server.js +122 -0
  85. package/dist/server.js.map +1 -0
  86. package/dist/tools/issues.d.ts +49 -0
  87. package/dist/tools/issues.js +132 -0
  88. package/dist/tools/issues.js.map +1 -0
  89. package/dist/tools/projects.d.ts +20 -0
  90. package/dist/tools/projects.js +15 -0
  91. package/dist/tools/projects.js.map +1 -0
  92. package/dist/tools/wiki.d.ts +47 -0
  93. package/dist/tools/wiki.js +86 -0
  94. package/dist/tools/wiki.js.map +1 -0
  95. package/dist/utils/error.d.ts +28 -0
  96. package/dist/utils/error.js +98 -0
  97. package/dist/utils/error.js.map +1 -0
  98. package/dist/utils/logger.d.ts +6 -0
  99. package/dist/utils/logger.js +18 -0
  100. package/dist/utils/logger.js.map +1 -0
  101. package/dist/utils/uuid.d.ts +2 -0
  102. package/dist/utils/uuid.js +15 -0
  103. package/dist/utils/uuid.js.map +1 -0
  104. package/package.json +66 -0
  105. package/scripts/postinstall.mjs +47 -0
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # trustasia-ones-mcp
2
+
3
+ 亚数 [ONES](https://ones.trustasia.cn) 的 Model Context Protocol (MCP) server,让 Claude Desktop / Claude Code / Cursor 能直接创建、修改需求,管理 Wiki 知识库。
4
+
5
+ - **一次配置永久无感**:钉钉手机号 + 密码自动登录,session 有效期最长 400 天
6
+ - **零代码安装**:一条 `npx` 命令搞定,无需 git clone、无需 `npm install`
7
+ - **跨机器复制即用**:Claude Desktop 配置同步到新机器,立刻可用
8
+
9
+ > 非技术同事:请直接跳到 [docs/USER_GUIDE.md](docs/USER_GUIDE.md),零代码前提的三步指南。
10
+
11
+ ---
12
+
13
+ ## 30 秒安装(产品同事版)
14
+
15
+ ```bash
16
+ # 1) 首装:输入手机号、密码,自动打通 Claude Desktop
17
+ npx -y trustasia-ones-mcp setup
18
+
19
+ # 2) 按提示重启 Claude Desktop,完成
20
+ ```
21
+
22
+ 常用命令:
23
+
24
+ ```bash
25
+ npx -y trustasia-ones-mcp status # 看登录状态、剩余 session 寿命
26
+ npx -y trustasia-ones-mcp login # 手动重新登录(密码改了用这个)
27
+ ```
28
+
29
+ ---
30
+
31
+ ## 工具列表(11 个)
32
+
33
+ ### 工作项
34
+
35
+ | 工具 | 说明 |
36
+ |---|---|
37
+ | `list_projects` | 列出团队下所有项目 |
38
+ | `list_issue_types` | 列出团队下所有工作项类型(需求/任务/缺陷等) |
39
+ | `create_issue` | 创建工作项 |
40
+ | `update_issue` | 修改工作项(标题/描述/状态/负责人/优先级) |
41
+ | `get_issue` | 按 UUID 查询单个工作项 |
42
+ | `search_issues` | 按项目/关键字/状态/类型搜索 |
43
+
44
+ ### Wiki
45
+
46
+ | 工具 | 说明 |
47
+ |---|---|
48
+ | `list_wiki_spaces` | 列出可访问的知识库空间 |
49
+ | `list_wiki_pages` | 列出空间下所有页面 |
50
+ | `create_wiki_page` | 创建草稿 + 设标题(正文需在浏览器手动粘贴+发布,原因见下) |
51
+ | `update_wiki_page` | 修改页面标题(正文同上限制) |
52
+ | `get_wiki_page` | 查看页面 |
53
+
54
+ > **Wiki 内容写入限制**:ONES Wiki 用 CRDT 协同编辑,正文写入需要 WebSocket 协议。
55
+ > 当前版本只能创建草稿、设标题,正文部分会渲染为 HTML 返回,请人工粘贴到浏览器编辑器后发布。
56
+
57
+ ---
58
+
59
+ ## 鉴权路径
60
+
61
+ `ones-mcp` 自动按优先级选择鉴权方式:
62
+
63
+ 1. `.env` 里有完整 `ONES_USER_ID + ONES_AUTH_TOKEN + ONES_TEAM_UUID` → 走静态 token(legacy)
64
+ 2. `.env` 或环境变量里有 `ONES_PHONE + ONES_PASSWORD` → **方案 D**:headless Playwright 自动登录钉钉,拿到 token 并缓存
65
+ 3. 本地 `~/.config/ones-mcp/session.json` 存在且有效 → silent refresh(400 天无感)
66
+ 4. 以上都没有 → 首次启动弹 headful 浏览器让用户钉钉扫码
67
+
68
+ 产品同事走第 2 条;开发者如果只想要一次性跑跑可以走第 1 条。
69
+
70
+ ---
71
+
72
+ ## 示例对话
73
+
74
+ > **用户**:帮我在 GoHTTPS 项目下建一个需求,标题"支持 IPv6",描述用 markdown 写一下需求点
75
+ >
76
+ > **Claude 自动调用**:
77
+ > 1. `list_projects` → 找到 GoHTTPS 的 UUID
78
+ > 2. `list_issue_types` → 找到"需求"类型 UUID
79
+ > 3. `create_issue` → 创建并返回新需求编号
80
+
81
+ ---
82
+
83
+ ## 开发者(从源码跑)
84
+
85
+ ```bash
86
+ git clone https://git.trustasia.cn/trustasia/ones-mcp.git
87
+ cd ones-mcp
88
+ npm install
89
+ cp .env.example .env # 按注释填 ONES_PHONE/ONES_PASSWORD 或 ONES_AUTH_TOKEN
90
+ npm run dev # 直接用 tsx 跑 stdio
91
+ npm run dev:http # HTTP server 模式(多人共享)
92
+ npm test
93
+ npm run typecheck
94
+ ```
95
+
96
+ ### 目录结构
97
+
98
+ ```
99
+ src/
100
+ ├── auth/ # 鉴权:CredentialProvider、DingTalk 登录自动化、Chromium 自愈
101
+ ├── cli/ # CLI 子命令:setup / login / status / stdio dispatcher
102
+ ├── client/ # ONES HTTP + GraphQL 封装
103
+ ├── tools/ # 11 个 MCP 工具实现
104
+ ├── schemas/ # Zod 输入校验
105
+ ├── utils/ # 错误、日志、UUID 生成
106
+ └── index.ts # stdio 入口(被 cli/index.ts 委托)
107
+ ```
108
+
109
+ ### HTTP server 模式(团队共享)
110
+
111
+ 一台服务器起 `ones-mcp`,每个同事各自在 Claude 配置里填 header 传自己的 token。见 [.env.server.example](.env.server.example)。
112
+
113
+ ```bash
114
+ cp .env.server.example .env
115
+ npm run start:http
116
+ # ones-mcp HTTP server ready on http://127.0.0.1:3000
117
+ ```
118
+
119
+ 每人在 `~/.claude.json` / `.mcp.json` 里写:
120
+
121
+ ```json
122
+ {
123
+ "mcpServers": {
124
+ "ones": {
125
+ "type": "http",
126
+ "url": "http://your-server:3000/mcp",
127
+ "headers": {
128
+ "X-Ones-User-Id": "自己的 uuid",
129
+ "X-Ones-Auth-Token": "自己的 token"
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ Token 抓取方法见 [scripts/refresh-token.md](scripts/refresh-token.md)(HTTP 模式下仍需 F12 手抓,或者在每个同事本机跑一次 `setup` 拿 token 回填 header)。
137
+
138
+ ---
139
+
140
+ ## 已知限制
141
+
142
+ - Wiki 正文写入需 CRDT WebSocket,当前不支持
143
+ - 钉钉风控触发滑块/SMS 时会从 headless 升级到 headful(用户需手动过一次挑战)
144
+ - 搜索的 keyword 是客户端过滤(先拉一批再筛),适合小数据量
145
+
146
+ ---
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,64 @@
1
+ import type { Credentials } from './types.js';
2
+ export interface BrowserAuthLoginOpts {
3
+ baseUrl: string;
4
+ storageStatePath: string;
5
+ timeoutMs?: number;
6
+ }
7
+ export interface BrowserAuthPhonePasswordOpts extends BrowserAuthLoginOpts {
8
+ phone: string;
9
+ password: string;
10
+ /**
11
+ * When true, DingTalk challenges (slider / SMS / risk control) surface as
12
+ * ChallengeRequiredError instead of blocking. When false, the browser is
13
+ * shown to the user and the automation waits for them to complete the
14
+ * challenge by hand.
15
+ */
16
+ headless: boolean;
17
+ }
18
+ export interface BrowserAuth {
19
+ loginInteractive(opts: BrowserAuthLoginOpts): Promise<Credentials>;
20
+ refreshSilent(opts: BrowserAuthLoginOpts): Promise<Credentials>;
21
+ loginWithPhonePassword(opts: BrowserAuthPhonePasswordOpts): Promise<Credentials>;
22
+ }
23
+ export interface CreatePlaywrightOpts {
24
+ executablePath?: string;
25
+ }
26
+ export declare function createPlaywrightBrowserAuth(opts?: CreatePlaywrightOpts): BrowserAuth;
27
+ interface MinimalPage {
28
+ goto(url: string, opts?: {
29
+ waitUntil?: 'domcontentloaded' | 'load' | 'networkidle';
30
+ }): Promise<unknown>;
31
+ waitForFunction<Arg>(pageFunction: (arg: Arg) => unknown, arg?: Arg, opts?: {
32
+ timeout?: number;
33
+ polling?: number | 'raf';
34
+ }): Promise<unknown>;
35
+ evaluate<T>(fn: () => T): Promise<T>;
36
+ content?(): Promise<string>;
37
+ locator(selector: string): {
38
+ count(): Promise<number>;
39
+ };
40
+ }
41
+ interface PlaywrightModule {
42
+ chromium: PlaywrightChromium;
43
+ }
44
+ interface PlaywrightChromium {
45
+ launch(opts?: {
46
+ headless?: boolean;
47
+ executablePath?: string;
48
+ }): Promise<PlaywrightBrowser>;
49
+ }
50
+ interface PlaywrightBrowser {
51
+ newContext(opts?: {
52
+ storageState?: string;
53
+ }): Promise<PlaywrightContext>;
54
+ close(): Promise<void>;
55
+ }
56
+ interface PlaywrightContext {
57
+ newPage(): Promise<MinimalPage>;
58
+ storageState(opts?: {
59
+ path?: string;
60
+ }): Promise<unknown>;
61
+ }
62
+ type PlaywrightImporter = () => Promise<PlaywrightModule>;
63
+ export declare function __setPlaywrightImporter(fn: PlaywrightImporter | null): void;
64
+ export {};
@@ -0,0 +1,327 @@
1
+ // Playwright-backed BrowserAuth. The real import lives inside the methods so
2
+ // that callers on the static-credentials path never pay the cost of loading
3
+ // playwright / downloading chromium.
4
+ //
5
+ // See docs/claude/specs/2026-04-20-dingtalk-auto-auth-design.md §4.4 and §8.1(2):
6
+ // the login-ready signal is derived from localStorage, not DOM selectors.
7
+ //
8
+ // Option-D extensions: `loginWithPhonePassword` runs DingTalk's phone+password
9
+ // login flow, escalating `headless -> ChallengeRequiredError` when DingTalk
10
+ // demands a human (see option-D design doc §6).
11
+ import { existsSync } from 'node:fs';
12
+ import { AuthCancelledError, AuthTimeoutError, ChallengeRequiredError, ChromiumMissingError, InvalidCredentialsError, PasswordLoginFailedError, SilentRefreshFailedError, } from './errors.js';
13
+ import { parseLocalStorageSnapshot } from './extract.js';
14
+ import { fillPhoneAndPassword, switchToPasswordTab, } from './dingtalk-login.js';
15
+ import { detectBadCredentials, detectChallenge, } from './challenge-detector.js';
16
+ import { ensureChromium } from './chromium-installer.js';
17
+ import { logger } from '../utils/logger.js';
18
+ import { redactPhone } from './config-redactor.js';
19
+ export function createPlaywrightBrowserAuth(opts = {}) {
20
+ return new PlaywrightBrowserAuth(opts);
21
+ }
22
+ class PlaywrightBrowserAuth {
23
+ opts;
24
+ constructor(opts) {
25
+ this.opts = opts;
26
+ }
27
+ async loginInteractive(o) {
28
+ const timeoutMs = o.timeoutMs ?? 180_000;
29
+ const { chromium } = await importPlaywright();
30
+ const browser = await launchWithHealing(chromium, {
31
+ headless: false,
32
+ executablePath: this.opts.executablePath,
33
+ });
34
+ try {
35
+ const context = await browser.newContext({
36
+ storageState: existsSync(o.storageStatePath) ? o.storageStatePath : undefined,
37
+ });
38
+ const page = await context.newPage();
39
+ await page.goto(o.baseUrl, { waitUntil: 'domcontentloaded' });
40
+ try {
41
+ await waitForLoginReady(page, timeoutMs);
42
+ }
43
+ catch (err) {
44
+ if (isTimeoutLike(err)) {
45
+ throw new AuthTimeoutError(`headful 登录超时(${timeoutMs}ms):请确认已完成钉钉扫码`);
46
+ }
47
+ if (isClosedLike(err)) {
48
+ throw new AuthCancelledError();
49
+ }
50
+ throw err;
51
+ }
52
+ const snapshot = await readLocalStorage(page);
53
+ const credentials = parseLocalStorageSnapshot(snapshot);
54
+ await context.storageState({ path: o.storageStatePath });
55
+ return credentials;
56
+ }
57
+ finally {
58
+ await browser.close().catch(() => undefined);
59
+ }
60
+ }
61
+ async refreshSilent(o) {
62
+ if (!existsSync(o.storageStatePath)) {
63
+ throw new SilentRefreshFailedError('storageState 文件不存在,无法静默续期(需要先完成一次交互登录)');
64
+ }
65
+ const timeoutMs = o.timeoutMs ?? 10_000;
66
+ const { chromium } = await importPlaywright();
67
+ const browser = await launchWithHealing(chromium, {
68
+ headless: true,
69
+ executablePath: this.opts.executablePath,
70
+ });
71
+ try {
72
+ const context = await browser.newContext({ storageState: o.storageStatePath });
73
+ const page = await context.newPage();
74
+ await page.goto(o.baseUrl, { waitUntil: 'domcontentloaded' });
75
+ try {
76
+ await waitForLoginReady(page, timeoutMs);
77
+ }
78
+ catch (err) {
79
+ if (isTimeoutLike(err)) {
80
+ throw new SilentRefreshFailedError(`静默续期超时(${timeoutMs}ms):storageState 可能已失效`);
81
+ }
82
+ throw err;
83
+ }
84
+ const snapshot = await readLocalStorage(page);
85
+ const credentials = parseLocalStorageSnapshot(snapshot);
86
+ // Persist refreshed storageState (cookies may rotate).
87
+ await context.storageState({ path: o.storageStatePath });
88
+ return credentials;
89
+ }
90
+ finally {
91
+ await browser.close().catch(() => undefined);
92
+ }
93
+ }
94
+ async loginWithPhonePassword(o) {
95
+ const maskedPhone = redactPhone(o.phone);
96
+ logger.info(`auth: password-login starting (phone=${maskedPhone}, headless=${o.headless})`);
97
+ const headlessTimeoutMs = o.timeoutMs ?? 30_000;
98
+ const headfulTimeoutMs = o.timeoutMs ?? 180_000;
99
+ const effectiveTimeout = o.headless ? headlessTimeoutMs : headfulTimeoutMs;
100
+ const { chromium } = await importPlaywright();
101
+ const browser = await launchWithHealing(chromium, {
102
+ headless: o.headless,
103
+ executablePath: this.opts.executablePath,
104
+ });
105
+ try {
106
+ const context = await browser.newContext({
107
+ storageState: existsSync(o.storageStatePath) ? o.storageStatePath : undefined,
108
+ });
109
+ const page = await context.newPage();
110
+ await page.goto(o.baseUrl, { waitUntil: 'domcontentloaded' });
111
+ // Short initial check: the ONES redirect to DingTalk may already be logged
112
+ // in (e.g., storageState valid). If waitForLoginReady resolves quickly,
113
+ // we can short-circuit without touching the login form at all.
114
+ const earlyReady = await raceLoginReady(page, 2_000);
115
+ if (earlyReady) {
116
+ const creds = await finalise(page, context, o.storageStatePath);
117
+ logger.info('auth: password-login skipped — session already valid');
118
+ return creds;
119
+ }
120
+ await switchToPasswordTab(page);
121
+ await fillPhoneAndPassword(page, {
122
+ phone: o.phone,
123
+ password: o.password,
124
+ });
125
+ // After submit, one of four things happens:
126
+ // a) login succeeds (localStorage populated)
127
+ // b) bad credentials toast appears
128
+ // c) DingTalk demands a human challenge
129
+ // d) network timeout / page still idle
130
+ // raceOutcome arbitrates between them.
131
+ const outcome = await raceOutcome(page, effectiveTimeout, o.headless);
132
+ switch (outcome.kind) {
133
+ case 'success': {
134
+ const creds = await finalise(page, context, o.storageStatePath);
135
+ logger.info('auth: password-login success');
136
+ return creds;
137
+ }
138
+ case 'bad-credentials': {
139
+ throw new InvalidCredentialsError('ONES 登录失败:钉钉提示手机号或密码错误,请在 Claude Desktop 配置中更新 ONES_PHONE / ONES_PASSWORD');
140
+ }
141
+ case 'challenge': {
142
+ if (o.headless) {
143
+ throw new ChallengeRequiredError(outcome.challengeKind);
144
+ }
145
+ // In headful mode, trust the user to solve it and wait longer.
146
+ const ok = await raceLoginReady(page, effectiveTimeout);
147
+ if (!ok) {
148
+ throw new AuthTimeoutError(`headful 登录超时(${effectiveTimeout}ms):请在弹出的浏览器完成验证`);
149
+ }
150
+ const creds = await finalise(page, context, o.storageStatePath);
151
+ return creds;
152
+ }
153
+ case 'timeout': {
154
+ throw new PasswordLoginFailedError(`钉钉登录页无响应(${effectiveTimeout}ms 内既未成功、也未报错)`);
155
+ }
156
+ }
157
+ }
158
+ finally {
159
+ await browser.close().catch(() => undefined);
160
+ }
161
+ }
162
+ }
163
+ async function finalise(page, context, storageStatePath) {
164
+ const snapshot = await readLocalStorage(page);
165
+ const credentials = parseLocalStorageSnapshot(snapshot);
166
+ await context.storageState({ path: storageStatePath });
167
+ return credentials;
168
+ }
169
+ async function raceOutcome(page, timeoutMs, _headless) {
170
+ const deadline = Date.now() + timeoutMs;
171
+ // Poll at 500ms, tight enough for responsive UX.
172
+ // eslint-disable-next-line no-constant-condition
173
+ while (true) {
174
+ const remaining = deadline - Date.now();
175
+ if (remaining <= 0)
176
+ return { kind: 'timeout' };
177
+ // 1) success first
178
+ const ready = await raceLoginReady(page, 500);
179
+ if (ready)
180
+ return { kind: 'success' };
181
+ const probe = pageToChallengeProbe(page);
182
+ // 2) bad credentials
183
+ // eslint-disable-next-line no-await-in-loop
184
+ const bad = await detectBadCredentials(probe).catch(() => false);
185
+ if (bad)
186
+ return { kind: 'bad-credentials' };
187
+ // 3) challenge
188
+ // eslint-disable-next-line no-await-in-loop
189
+ const kind = await detectChallenge(probe).catch(() => null);
190
+ if (kind)
191
+ return { kind: 'challenge', challengeKind: kind };
192
+ }
193
+ }
194
+ function pageToChallengeProbe(page) {
195
+ return {
196
+ async evalText() {
197
+ try {
198
+ if (typeof page.content !== 'function')
199
+ return '';
200
+ const html = await page.content();
201
+ return html ?? '';
202
+ }
203
+ catch {
204
+ return '';
205
+ }
206
+ },
207
+ async cssMatches(selector) {
208
+ try {
209
+ const loc = page.locator(selector);
210
+ const n = await loc.count();
211
+ return n > 0;
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ },
217
+ };
218
+ }
219
+ async function raceLoginReady(page, timeoutMs) {
220
+ try {
221
+ await waitForLoginReady(page, timeoutMs);
222
+ return true;
223
+ }
224
+ catch {
225
+ return false;
226
+ }
227
+ }
228
+ async function waitForLoginReady(page, timeoutMs) {
229
+ await page.waitForFunction(() => {
230
+ try {
231
+ const raw = localStorage.getItem('_ones_json_login_info');
232
+ if (!raw)
233
+ return false;
234
+ const parsed = JSON.parse(raw);
235
+ if (typeof parsed?.uuid !== 'string' ||
236
+ parsed.uuid.length === 0 ||
237
+ typeof parsed?.token !== 'string' ||
238
+ parsed.token.length === 0) {
239
+ return false;
240
+ }
241
+ const teamUuid = localStorage.getItem('teamUUID');
242
+ return typeof teamUuid === 'string' && teamUuid.length > 0;
243
+ }
244
+ catch {
245
+ return false;
246
+ }
247
+ }, undefined, { timeout: timeoutMs, polling: 500 });
248
+ }
249
+ async function readLocalStorage(page) {
250
+ const snapshot = await page.evaluate(() => {
251
+ const out = {};
252
+ for (let i = 0; i < localStorage.length; i++) {
253
+ const k = localStorage.key(i);
254
+ if (k)
255
+ out[k] = localStorage.getItem(k);
256
+ }
257
+ return out;
258
+ });
259
+ const normalised = {};
260
+ for (const [k, v] of Object.entries(snapshot)) {
261
+ normalised[k] = v ?? undefined;
262
+ }
263
+ return normalised;
264
+ }
265
+ let playwrightImporter = null;
266
+ export function __setPlaywrightImporter(fn) {
267
+ playwrightImporter = fn;
268
+ }
269
+ async function importPlaywright() {
270
+ if (playwrightImporter) {
271
+ return playwrightImporter();
272
+ }
273
+ try {
274
+ // @ts-ignore — optional runtime dep; may not be installed in all environments.
275
+ const mod = await import('playwright');
276
+ return mod;
277
+ }
278
+ catch (err) {
279
+ throw new Error(`无法加载 playwright 模块。请运行 \`npm install\` 并确认 chromium 已安装:\n` +
280
+ ` npx playwright install chromium\n` +
281
+ `原始错误:${err instanceof Error ? err.message : String(err)}`);
282
+ }
283
+ }
284
+ async function launchWithHealing(chromium, opts) {
285
+ try {
286
+ return await chromium.launch(opts);
287
+ }
288
+ catch (err) {
289
+ if (!isChromiumMissing(err))
290
+ throw err;
291
+ logger.warn('Chromium 未安装,正在自动下载(约 92 MB,1-5 分钟)…');
292
+ try {
293
+ await ensureChromium({
294
+ onProgress: (line) => logger.info(`playwright: ${line}`),
295
+ });
296
+ }
297
+ catch (installErr) {
298
+ throw new ChromiumMissingError(installErr instanceof Error ? installErr.message : String(installErr));
299
+ }
300
+ // Retry once.
301
+ return chromium.launch(opts);
302
+ }
303
+ }
304
+ function isChromiumMissing(err) {
305
+ if (!(err instanceof Error))
306
+ return false;
307
+ const msg = err.message ?? '';
308
+ if (/Executable doesn't exist/i.test(msg))
309
+ return true;
310
+ if (/browserType\.launch/i.test(msg) && /chromium/i.test(msg))
311
+ return true;
312
+ const code = err.code;
313
+ if (code === 'ENOENT' && /playwright/i.test(msg))
314
+ return true;
315
+ return false;
316
+ }
317
+ function isTimeoutLike(err) {
318
+ if (!(err instanceof Error))
319
+ return false;
320
+ return /Timeout|timed out/i.test(err.message);
321
+ }
322
+ function isClosedLike(err) {
323
+ if (!(err instanceof Error))
324
+ return false;
325
+ return /closed|Target closed|Page closed|Browser has been closed/i.test(err.message);
326
+ }
327
+ //# sourceMappingURL=browser-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-auth.js","sourceRoot":"","sources":["../../src/auth/browser-auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { ChallengeKind } from './types.js';
2
+ /**
3
+ * Abstract Page probe the detector calls into. Keeping this small makes the
4
+ * detector trivial to unit-test without booting Playwright.
5
+ */
6
+ export interface ChallengeProbe {
7
+ /** Return the visible text content of the page (innerText of body). */
8
+ evalText(): Promise<string>;
9
+ /** Return whether a CSS selector matches at least one element. */
10
+ cssMatches(selector: string): Promise<boolean>;
11
+ }
12
+ /**
13
+ * Identify whether the current DingTalk login page is asking for an additional
14
+ * human challenge. Returns `null` when the page is clean, or the
15
+ * {@link ChallengeKind} of the first matching rule.
16
+ */
17
+ export declare function detectChallenge(probe: ChallengeProbe): Promise<ChallengeKind | null>;
18
+ /**
19
+ * True when the page surfaces a "wrong password / account not found" toast.
20
+ * Callers treat this as a terminal {@link InvalidCredentialsError}.
21
+ */
22
+ export declare function detectBadCredentials(probe: ChallengeProbe): Promise<boolean>;
@@ -0,0 +1,75 @@
1
+ // DingTalk login challenge detection.
2
+ //
3
+ // The detectors are deliberately *probe-based* — they never throw, never wait
4
+ // on long timeouts, and never depend on DingTalk-specific CSS class names
5
+ // (which change between DingTalk front-end versions). We combine:
6
+ // 1) a plain-text body sample from the page, matched against stable Chinese
7
+ // phrasing the user actually sees ("密码错误", "滑块", "短信验证");
8
+ // 2) a small set of wide-open CSS hints (`[class*="slider"]`) as a fallback
9
+ // for cases where the DOM already contains the challenge widget but the
10
+ // page text hasn't surfaced yet.
11
+ //
12
+ // See docs/claude/specs/2026-04-21-option-d-phone-password-auth-design.md §6.3.
13
+ // Order matters: more specific probes first. If two rules fire, the earlier
14
+ // entry wins.
15
+ const RULES = [
16
+ {
17
+ kind: 'risk-control',
18
+ text: /异常登录|账号保护|安全验证(?!码)/,
19
+ },
20
+ {
21
+ kind: 'image-captcha',
22
+ text: /图片验证码|请输入图片验证码/,
23
+ css: ['img[alt*="captcha"]', 'img[src*="captcha"]'],
24
+ },
25
+ {
26
+ kind: 'sms',
27
+ text: /短信验证|短信验证码|请输入验证码/,
28
+ },
29
+ {
30
+ kind: 'slider',
31
+ text: /拖动滑块|滑动验证|滑块验证/,
32
+ css: ['[class*="nc_"]', '[class*="slider"]', '[class*="captcha_slide"]'],
33
+ },
34
+ ];
35
+ /**
36
+ * Identify whether the current DingTalk login page is asking for an additional
37
+ * human challenge. Returns `null` when the page is clean, or the
38
+ * {@link ChallengeKind} of the first matching rule.
39
+ */
40
+ export async function detectChallenge(probe) {
41
+ const [text] = await Promise.all([probe.evalText()]);
42
+ for (const rule of RULES) {
43
+ if (rule.text && rule.text.test(text)) {
44
+ return rule.kind;
45
+ }
46
+ if (rule.css) {
47
+ for (const selector of rule.css) {
48
+ // Await sequentially: we want to bail as soon as one matches; a
49
+ // parallel map would waste a Playwright round-trip on the remainder.
50
+ // eslint-disable-next-line no-await-in-loop
51
+ const hit = await probe.cssMatches(selector);
52
+ if (hit)
53
+ return rule.kind;
54
+ }
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ const BAD_CREDENTIAL_PATTERNS = [
60
+ /密码错误/,
61
+ /账号或密码不正确/,
62
+ /账号或密码错误/,
63
+ /手机号未注册/,
64
+ /该手机号未注册/,
65
+ /用户不存在/,
66
+ ];
67
+ /**
68
+ * True when the page surfaces a "wrong password / account not found" toast.
69
+ * Callers treat this as a terminal {@link InvalidCredentialsError}.
70
+ */
71
+ export async function detectBadCredentials(probe) {
72
+ const text = await probe.evalText();
73
+ return BAD_CREDENTIAL_PATTERNS.some((p) => p.test(text));
74
+ }
75
+ //# sourceMappingURL=challenge-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"challenge-detector.js","sourceRoot":"","sources":["../../src/auth/challenge-detector.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,kEAAkE;AAClE,8EAA8E;AAC9E,+DAA+D;AAC/D,8EAA8E;AAC9E,6EAA6E;AAC7E,sCAAsC;AACtC,EAAE;AACF,gFAAgF;AAqBhF,4EAA4E;AAC5E,cAAc;AACd,MAAM,KAAK,GAAkB;IAC3B;QACE,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,qBAAqB;KAC5B;IACD;QACE,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,gBAAgB;QACtB,GAAG,EAAE,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;KACpD;IACD;QACE,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,mBAAmB;KAC1B;IACD;QACE,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,gBAAgB;QACtB,GAAG,EAAE,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,0BAA0B,CAAC;KACzE;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,KAAqB;IAErB,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAErD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,IAAI,CAAC;QACnB,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;gBAChC,gEAAgE;gBAChE,qEAAqE;gBACrE,4CAA4C;gBAC5C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;gBAC7C,IAAI,GAAG;oBAAE,OAAO,IAAI,CAAC,IAAI,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,uBAAuB,GAAa;IACxC,MAAM;IACN,UAAU;IACV,SAAS;IACT,QAAQ;IACR,SAAS;IACT,OAAO;CACR,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAqB;IAErB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC;IACpC,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ChildProcess, SpawnOptions } from 'node:child_process';
2
+ export interface EnsureChromiumOpts {
3
+ onProgress?: (line: string) => void;
4
+ /** Default 10 minutes. */
5
+ timeoutMs?: number;
6
+ }
7
+ type SpawnImpl = (cmd: string, args: readonly string[], opts?: SpawnOptions) => ChildProcess;
8
+ /** Test hook: override `spawn`. Pass `null` to restore the real implementation. */
9
+ export declare function __setSpawnImpl(impl: SpawnImpl | null): void;
10
+ export declare function ensureChromium(opts?: EnsureChromiumOpts): Promise<void>;
11
+ export {};