weaver-work-cli 0.1.23 → 0.1.24

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 (41) hide show
  1. package/README.md +23 -2
  2. package/dist/internal/e10/auth/commands.js +194 -16
  3. package/dist/internal/e10/auth/xiaoe.js +15 -6
  4. package/dist/internal/e10/context.js +3 -3
  5. package/dist/internal/e10/request-runtime.js +46 -4
  6. package/dist/shortcuts/caizhilian/file-upload/metadata.js +124 -0
  7. package/dist/shortcuts/caizhilian/file-upload/multipart.js +47 -0
  8. package/dist/shortcuts/caizhilian/file-upload/protocol.js +116 -0
  9. package/dist/shortcuts/caizhilian/file-upload/receipt.js +37 -0
  10. package/dist/shortcuts/caizhilian/host.js +13 -5
  11. package/dist/shortcuts/caizhilian/operations/handlers/file-upload.js +72 -179
  12. package/dist/shortcuts/caizhilian/operations/handlers/folder.js +701 -4
  13. package/dist/shortcuts/caizhilian/operations/registry.js +5 -1
  14. package/dist/shortcuts/caizhilian/operations/specs.js +52 -0
  15. package/dist/shortcuts/workflow/continuation.js +14 -9
  16. package/dist/shortcuts/workflow/endpoints.js +11 -1
  17. package/dist/shortcuts/workflow/operations/flow-status.js +63 -0
  18. package/dist/shortcuts/workflow/operations/form.js +650 -518
  19. package/dist/shortcuts/workflow/operations/read.js +374 -13
  20. package/dist/shortcuts/workflow/operations/shared.js +3 -0
  21. package/dist/shortcuts/workflow/operations/write.js +5 -5
  22. package/dist/shortcuts/workflow/operations.js +8 -6
  23. package/docs/caizhilian.md +8 -2
  24. package/docs/e10-auth.md +18 -3
  25. package/package.json +1 -1
  26. package/skills/weaver-e10-caizhilian/SKILL.md +8 -3
  27. package/skills/weaver-e10-caizhilian/references/file-upload.md +100 -295
  28. package/skills/weaver-e10-caizhilian/references/folder.md +31 -3
  29. package/skills/weaver-e10-caizhilian/references/source-manifest.json +536 -25
  30. package/skills/weaver-e10-jiuchuanhui/SKILL.md +9 -1
  31. package/skills/weaver-e10-shared/SKILL.md +12 -2
  32. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +21 -3
  33. package/skills/weaver-e10-workflow/SKILL.md +10 -8
  34. package/skills/weaver-e10-workflow/references/source-manifest.json +36 -31
  35. package/skills/weaver-e10-workflow/references/workflow-batch.md +2 -4
  36. package/skills/weaver-e10-workflow/references/workflow-create.md +58 -36
  37. package/skills/weaver-e10-workflow/references/workflow-edit.md +2 -1
  38. package/skills/weaver-e10-workflow/references/workflow-memory.md +2 -2
  39. package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -1
  40. package/skills/weaver-e10-workflow/references/workflow-query.md +9 -7
  41. package/skills/weaver-e10-workflow/references/workflow-share.md +2 -0
package/README.md CHANGED
@@ -211,7 +211,7 @@ weaver-work-cli auth login --base-url "https://weapp.xxx.cn"
211
211
  weaver-work-cli auth oidc --base-url "https://weapp.xxx.cn"
212
212
  ```
213
213
 
214
- 在 XiaoE 宿主环境中,`auth login` / `auth oidc` / `auth xiaoe` 会优先使用平台注入的
214
+ 只有当前智能体身份明确为 XiaoE 时,`auth xiaoe` 和未显式指定认证来源的业务命令才会优先使用平台注入的
215
215
  `XIAOE_USER_TOKEN` + `XIAOE_DOMAIN` 换取 ETEAMSID:其中 `XIAOE_DOMAIN` 是
216
216
  XiaoE/erpa 换票服务地址,不是 E10 登录地址。CLI 的 Node 实现会直接调用与
217
217
  `xiaoe_env.py` / `tokens.py` 相同的换票接口,并校验业务码和 `data.eteamId`;它保留
@@ -226,7 +226,7 @@ Agent;Skill 只负责调用 `weaver-work-cli auth xiaoe`。CLI 自带脚本仅
226
226
  E10 目标地址按“显式 `--base-url` → 专用 `WEAVER_XIAOE_*` E10 配置/映射 →
227
227
  当前 profile → `https://www.e-cology.com.cn`”解析,绝不会把 `XIAOE_DOMAIN`
228
228
  当作 E10 地址。匹配成功时直接保存登录态,并固定使用 `agentType=XiaoE`。业务命令
229
- 在未显式指定 `--auth` / `--profile` 时,也会自动优先尝试 XiaoE 统一登录。
229
+ 在已确认的 XiaoE 智能体环境中且未显式指定 `--auth` / `--profile` 时,也会自动优先尝试 XiaoE 统一登录。
230
230
  `auth xiaoe` 失败后仅在 macOS/Windows 桌面宿主自动降级浏览器 OAuth;Web/Linux
231
231
  环境会返回结构化的 `authentication/xiaoe_web_credential_unavailable` 错误,不再
232
232
  启动无法回调的 localhost 浏览器登录。确需浏览器授权时,应在可接收本地回调的宿主
@@ -236,6 +236,8 @@ E10 目标地址按“显式 `--base-url` → 专用 `WEAVER_XIAOE_*` E10 配置
236
236
  weaver-work-cli auth xiaoe
237
237
  ```
238
238
 
239
+ `auth login` / `auth oidc` 始终是纯浏览器 OAuth 入口。本机存在 `xiaoeUserData/tokens.py`、XiaoE 客户端目录、`XIAOE_SKILL_*` 或 `WEAVER_XIAOE_*` 配置,都不能单独证明当前智能体是 XiaoE,也不会让 WorkBuddy、Codex 或其他智能体切换到 XiaoE 登录。
240
+
239
241
  默认本地回调地址为 `http://127.0.0.1:19800/callback`。如果端口被占用:
240
242
 
241
243
  ```text
@@ -252,6 +254,25 @@ weaver-work-cli auth login --base-url "https://weapp.xxx.cn" --port 19801
252
254
  weaver-work-cli auth login --base-url "https://weapp.xxx.cn" --agent_type workbuddy
253
255
  ```
254
256
 
257
+ 浏览器授权成功后,本地回调页会为已识别的桌面智能体显示应用打开弹层。CLI 内置
258
+ WorkBuddy(`workbuddy://`)、智能小 E(`xiaoe-client://`)和 Codex(`codex://`)映射;
259
+ 其他智能体在 `agent_type` 本身是合法协议名时,默认按 `<agent_type>://` 唤起。弹层显示
260
+ “打开客户端 / 始终打开客户端 / 取消”。用户确认后,页面通过一次性令牌请求本地 CLI,
261
+ 由 CLI 调用系统注册的桌面协议;浏览器不直接访问自定义
262
+ 协议,因此不会再出现 Chrome/Edge 无法定制的外部协议确认框。Windows/macOS 安装包都
263
+ 需要注册该协议。如果宿主要返回当前会话等更精确的位置,可传入宿主
264
+ 注册的深链(仅接受桌面应用自定义协议,不接受网页、脚本或本地文件 URL):
265
+
266
+ ```text
267
+ weaver-work-cli auth login --base-url "https://weapp.xxx.cn" --agent_type workbuddy --agent-return-url "workbuddy://<宿主提供的路径>"
268
+ ```
269
+
270
+ 宿主也可以注入 `WEAVER_AGENT_RETURN_URL`,命令行参数优先于环境变量;客户端协议名与
271
+ `agent_type` 不一致时也应使用该覆盖。“始终打开”偏好
272
+ 保存在当前本地回调地址的浏览器存储中;未识别到可用返回协议时,回调页保持原有提示,
273
+ 不显示应用打开弹层。本地 CLI 打开通道默认保留 15 分钟;超过时间再点击时会自动退回
274
+ 浏览器自定义协议打开方式,仍可打开应用,但浏览器可能重新显示原生协议确认框。
275
+
255
276
  方式二:手动写入浏览器中的 `ETEAMSID`。
256
277
 
257
278
  ```text
@@ -1,4 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
2
3
  import { createServer } from 'node:http';
3
4
  import { CliError, ExitCode } from '../../../core/errors.js';
4
5
  import { clearInvoiceTokenForSession } from '../../../shortcuts/invoice/host.js';
@@ -8,9 +9,46 @@ import { XIAOE_AGENT_TYPE, createXiaoEUnifiedLoginUnavailableError, hasXiaoEEnv,
8
9
  export { DEFAULT_E10_BASE_URL, XIAOE_AGENT_TYPE, hasXiaoEEnv, hasXiaoETokenScript, resolveXiaoEDefaultBaseUrl, resolveXiaoEPreferredBaseUrl, resolveXiaoEUnifiedLoginCredential, shouldUseXiaoEBrowserFallback, } from './xiaoe.js';
9
10
  const DEFAULT_AGENT_TYPE = 'weaver-work-cli';
10
11
  const EXPLICIT_AGENT_TYPE_ENV_KEYS = [
11
- 'WEAVER_AGENT_TYPE',
12
12
  'WORKBUDDY_AGENT_TYPE',
13
13
  'CODEX_AGENT_TYPE',
14
+ 'WEAVER_AGENT_TYPE',
15
+ ];
16
+ const AGENT_RETURN_URL_ENV_KEY = 'WEAVER_AGENT_RETURN_URL';
17
+ const AGENT_RETURN_PATH = '/agent-return';
18
+ const AGENT_RETURN_TOKEN_HEADER = 'x-weaver-agent-return-token';
19
+ const AGENT_RETURN_TIMEOUT_MS = 900_000;
20
+ const UNSAFE_AGENT_RETURN_PROTOCOLS = new Set([
21
+ 'about:',
22
+ 'blob:',
23
+ 'data:',
24
+ 'file:',
25
+ 'ftp:',
26
+ 'http:',
27
+ 'https:',
28
+ 'javascript:',
29
+ 'mailto:',
30
+ 'sms:',
31
+ 'tel:',
32
+ 'vbscript:',
33
+ 'ws:',
34
+ 'wss:',
35
+ ]);
36
+ const BUILT_IN_AGENT_RETURN_TARGETS = [
37
+ {
38
+ aliases: ['workbuddy'],
39
+ label: 'WorkBuddy',
40
+ url: 'workbuddy://',
41
+ },
42
+ {
43
+ aliases: ['xiaoe', 'xiaoeclient', '小e', '智能小e'],
44
+ label: '智能小 E',
45
+ url: 'xiaoe-client://',
46
+ },
47
+ {
48
+ aliases: ['codex', 'openaicodex'],
49
+ label: 'Codex',
50
+ url: 'codex://',
51
+ },
14
52
  ];
15
53
  function asOptions(value) {
16
54
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -49,6 +87,59 @@ export function resolveAgentType(options = {}, env = process.env) {
49
87
  || inferAgentTypeFromEnv(env)
50
88
  || DEFAULT_AGENT_TYPE;
51
89
  }
90
+ function normalizeAgentKey(value) {
91
+ return value.toLowerCase().replace(/[^a-z0-9]+/gu, '');
92
+ }
93
+ function normalizeAgentAlias(value) {
94
+ return value.trim().toLowerCase().replace(/[^\p{Letter}\p{Number}]+/gu, '');
95
+ }
96
+ function builtInAgentReturnTarget(agentType) {
97
+ const alias = normalizeAgentAlias(agentType);
98
+ const target = BUILT_IN_AGENT_RETURN_TARGETS.find((candidate) => candidate.aliases.includes(alias));
99
+ return target ? { label: target.label, url: target.url } : null;
100
+ }
101
+ function conventionalAgentReturnUrl(agentType) {
102
+ const scheme = agentType.trim().toLowerCase();
103
+ if (normalizeAgentAlias(agentType) === normalizeAgentAlias(DEFAULT_AGENT_TYPE)
104
+ || scheme.length > 64
105
+ || !/^[a-z][a-z0-9+.-]*$/u.test(scheme)
106
+ || UNSAFE_AGENT_RETURN_PROTOCOLS.has(`${scheme}:`)) {
107
+ return '';
108
+ }
109
+ return `${scheme}://`;
110
+ }
111
+ function validateAgentReturnUrl(value) {
112
+ if (value.length > 2048
113
+ || /[\u0000-\u001f\u007f\s]/u.test(value)
114
+ || !/^[a-z][a-z0-9+.-]*:\/\//iu.test(value)) {
115
+ throw new CliError('Invalid --agent-return-url value', ExitCode.UsageError);
116
+ }
117
+ let url;
118
+ try {
119
+ url = new URL(value);
120
+ }
121
+ catch {
122
+ throw new CliError('Invalid --agent-return-url value', ExitCode.UsageError);
123
+ }
124
+ if (UNSAFE_AGENT_RETURN_PROTOCOLS.has(url.protocol)) {
125
+ throw new CliError('--agent-return-url must use a registered desktop application URL scheme.', ExitCode.UsageError);
126
+ }
127
+ return value;
128
+ }
129
+ export function resolveAgentReturnTarget(options = {}, env = process.env, agentType = resolveAgentType(options, env)) {
130
+ const builtInTarget = builtInAgentReturnTarget(agentType);
131
+ const explicitUrl = optionString(options, 'agentReturnUrl') || envString(env, AGENT_RETURN_URL_ENV_KEY);
132
+ if (explicitUrl) {
133
+ return {
134
+ label: builtInTarget?.label || agentType,
135
+ url: validateAgentReturnUrl(explicitUrl),
136
+ };
137
+ }
138
+ if (builtInTarget)
139
+ return builtInTarget;
140
+ const conventionalUrl = conventionalAgentReturnUrl(agentType);
141
+ return conventionalUrl ? { label: agentType, url: conventionalUrl } : null;
142
+ }
52
143
  function normalizeUrl(value) {
53
144
  return value.replace(/\/+$/, '');
54
145
  }
@@ -160,8 +251,18 @@ function esc(value) {
160
251
  .replace(/"/g, '&quot;')
161
252
  .replace(/'/g, '&#39;');
162
253
  }
163
- export function renderE10LoginSuccessPage(userId) {
164
- return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>weaver-work-cli 登录成功</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;background:#f6f7f9;color:#20242a;display:grid;place-items:center;min-height:100vh;margin:0}.card{background:#fff;border:1px solid #e6e8ec;border-radius:8px;padding:28px 32px;box-shadow:0 8px 32px rgba(18,24,40,.08);min-width:320px;text-align:center}.title{font-size:20px;font-weight:600;margin-bottom:8px}.desc{color:#667085;font-size:14px;line-height:22px}.user{color:#98a2b3;font-size:12px;margin-top:12px}</style></head><body><main class="card"><div class="title">登录授权成功</div><div class="desc">你可以关闭此页面,返回智能体或终端继续使用。</div><div class="user">${esc(userId)}</div></main></body></html>`;
254
+ export function renderE10LoginSuccessPage(userId, returnTarget = null, launchToken = '') {
255
+ const safeReturnTarget = returnTarget
256
+ ? { ...returnTarget, url: validateAgentReturnUrl(returnTarget.url) }
257
+ : null;
258
+ const agentKey = safeReturnTarget ? normalizeAgentKey(safeReturnTarget.label) || 'agent' : '';
259
+ const returnAction = safeReturnTarget && launchToken
260
+ ? `<div class="overlay" data-agent-return data-token="${esc(launchToken)}" data-agent-key="${esc(agentKey)}" data-agent-label="${esc(safeReturnTarget.label)}" data-fallback-url="${esc(safeReturnTarget.url)}"><section class="prompt" role="dialog" aria-modal="true" aria-labelledby="agent-return-title"><div class="app-icon" aria-hidden="true"><svg viewBox="0 0 64 64"><defs><linearGradient id="app-gradient" x1="8" y1="4" x2="56" y2="60" gradientUnits="userSpaceOnUse"><stop stop-color="#24d3b5"/><stop offset="1" stop-color="#08aa82"/></linearGradient></defs><rect width="64" height="64" rx="16" fill="url(#app-gradient)"/><path d="M18 21.5 32 13l14 8.5v17L32 47 18 38.5v-17Z" fill="#fff" fill-opacity=".96"/><rect x="23" y="25" width="18" height="15" rx="6" fill="#12b991"/><circle cx="28.5" cy="32.5" r="1.8" fill="#fff"/><circle cx="35.5" cy="32.5" r="1.8" fill="#fff"/></svg></div><h1 id="agent-return-title">是否打开“${esc(safeReturnTarget.label)}”?</h1><p>登录授权已完成,返回 ${esc(safeReturnTarget.label)} 继续使用。</p><button class="primary" type="button" data-action="open">打开“${esc(safeReturnTarget.label)}”</button><button class="secondary" type="button" data-action="always">始终打开“${esc(safeReturnTarget.label)}”</button><button class="secondary" type="button" data-action="cancel">取消</button><div class="status" role="status" aria-live="polite"></div></section></div>`
261
+ : '';
262
+ const returnScript = safeReturnTarget && launchToken
263
+ ? `<script nonce="${esc(launchToken)}">(()=>{const root=document.querySelector('[data-agent-return]');if(!root)return;const status=root.querySelector('.status');const buttons=[...root.querySelectorAll('button')];const agentLabel=root.dataset.agentLabel||'应用';const fallbackUrl=root.dataset.fallbackUrl||'';const storageKey='weaver-work-cli.agent-return.'+root.dataset.agentKey;let busy=false;const setBusy=value=>{busy=value;buttons.forEach(button=>{button.disabled=value;});};const closePrompt=()=>{root.hidden=true;};const savePreference=()=>{try{localStorage.setItem(storageKey,'1');}catch{}};const clearPreference=()=>{try{localStorage.removeItem(storageKey);}catch{}};const hasPreference=()=>{try{return localStorage.getItem(storageKey)==='1';}catch{return false;}};const post=async action=>{let response;try{response=await fetch('${AGENT_RETURN_PATH}?action='+encodeURIComponent(action),{method:'POST',headers:{'${AGENT_RETURN_TOKEN_HEADER}':root.dataset.token}});}catch{return false;}if(!response.ok)throw new Error(await response.text()||'请求失败');return true;};const openAgent=async persist=>{if(busy)return;setBusy(true);status.textContent='正在打开 '+agentLabel+'…';try{const openedLocally=await post('open');if(persist)savePreference();if(!openedLocally){closePrompt();window.location.href=fallbackUrl;return;}closePrompt();}catch(error){if(persist)clearPreference();status.textContent=error instanceof Error?error.message:'无法打开应用,请稍后重试。';setBusy(false);}};root.querySelector('[data-action="open"]').addEventListener('click',()=>openAgent(false));root.querySelector('[data-action="always"]').addEventListener('click',()=>openAgent(true));root.querySelector('[data-action="cancel"]').addEventListener('click',()=>{if(busy)return;clearPreference();closePrompt();post('cancel').catch(()=>{});});if(hasPreference())openAgent(false);})();</script>`
264
+ : '';
265
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>weaver-work-cli 登录成功</title><style>:root{color-scheme:light}*{box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;background:radial-gradient(circle at 50% 18%,#eefbf8 0,#f6f7f9 42%,#eef1f5 100%);color:#20242a;display:grid;place-items:center;min-height:100vh;margin:0}.card{background:rgba(255,255,255,.88);border:1px solid #e6e8ec;border-radius:18px;padding:34px 38px;box-shadow:0 18px 55px rgba(18,24,40,.09);width:min(560px,calc(100vw - 40px));text-align:center}.title{font-size:24px;font-weight:700;margin-bottom:10px}.desc{color:#667085;font-size:15px;line-height:24px}.user{color:#98a2b3;font-size:12px;margin-top:18px}.overlay{position:fixed;inset:0;z-index:10;display:grid;place-items:center;padding:20px;background:rgba(28,34,41,.36);backdrop-filter:blur(5px)}.overlay[hidden]{display:none}.prompt{width:min(410px,calc(100vw - 32px));padding:32px 28px 26px;border:1px solid rgba(255,255,255,.75);border-radius:24px;background:rgba(250,250,251,.97);box-shadow:0 24px 70px rgba(15,23,42,.28);text-align:center}.app-icon{width:74px;height:74px;margin:0 auto 24px;filter:drop-shadow(0 8px 14px rgba(8,170,130,.22))}.app-icon svg{display:block;width:100%;height:100%}.prompt h1{margin:0;font-size:20px;line-height:30px;font-weight:700;letter-spacing:-.2px}.prompt p{margin:10px 0 24px;color:#667085;font-size:14px;line-height:22px}.prompt button{display:block;width:100%;min-height:40px;border:0;border-radius:12px;font:600 16px/22px inherit;cursor:pointer;transition:transform .12s ease,filter .12s ease}.prompt button+button{margin-top:10px}.prompt button:hover{filter:brightness(.98)}.prompt button:active{transform:scale(.99)}.prompt button:disabled{cursor:wait;opacity:.62}.primary{background:linear-gradient(180deg,#2f91ff,#0b74e5);color:#fff;box-shadow:0 8px 18px rgba(11,116,229,.2)}.secondary{background:#e9eaec;color:#22252a}.status{min-height:18px;margin-top:14px;color:#667085;font-size:12px;line-height:18px}</style></head><body><main class="card"><div class="title">登录授权成功</div><div class="desc">你可以关闭此页面,返回智能体或终端继续使用。</div><div class="user">${esc(userId)}</div></main>${returnAction}${returnScript}</body></html>`;
165
266
  }
166
267
  export function resolveBrowserOpenCommand(url, platform = process.platform) {
167
268
  if (platform === 'win32') {
@@ -179,6 +280,17 @@ function openUrlInBrowser(url, ctx) {
179
280
  }
180
281
  });
181
282
  }
283
+ function openUrlWithSystemHandler(url) {
284
+ const command = resolveBrowserOpenCommand(url);
285
+ return new Promise((resolve, reject) => {
286
+ execFile(command.file, command.args, (error) => {
287
+ if (error)
288
+ reject(error);
289
+ else
290
+ resolve();
291
+ });
292
+ });
293
+ }
182
294
  export function buildE10AuthorizeUrl(baseUrl, redirectUri, agentType) {
183
295
  const authorizeParams = new URLSearchParams({
184
296
  redirect_uri: redirectUri,
@@ -380,11 +492,14 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
380
492
  const profileFromGlobal = globalOptions.profile;
381
493
  const cfg = readProfileConfig(profileFromGlobal || getActiveProfile());
382
494
  const explicitBaseUrl = optionString(commandOpts, 'baseUrl') || globalOptions.baseUrl || '';
383
- const baseUrl = resolveXiaoEPreferredBaseUrl({
384
- explicitBaseUrl,
385
- profileBaseUrl: cfg.baseUrl,
386
- env: ctx.env,
387
- });
495
+ const useXiaoELogin = behavior.tryXiaoEUnifiedLogin === true && hasXiaoEEnv(ctx.env);
496
+ const baseUrl = useXiaoELogin
497
+ ? resolveXiaoEPreferredBaseUrl({
498
+ explicitBaseUrl,
499
+ profileBaseUrl: cfg.baseUrl,
500
+ env: ctx.env,
501
+ })
502
+ : normalizeUrl(explicitBaseUrl || cfg.baseUrl || '');
388
503
  const tenantKey = optionString(commandOpts, 'tenantKey');
389
504
  const portText = optionString(commandOpts, 'port') || '19800';
390
505
  const port = Number.parseInt(portText, 10);
@@ -393,11 +508,15 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
393
508
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
394
509
  throw new CliError('Invalid --port value', ExitCode.UsageError);
395
510
  }
396
- const xiaoEResult = await tryXiaoEUnifiedLogin(ctx, baseUrl, tenantKey, profileFromGlobal, behavior.allowXiaoETokenScript === true);
397
- if (xiaoEResult)
398
- return xiaoEResult;
399
- const willOpenBrowser = behavior.browserFallback !== 'desktop-only' || shouldUseXiaoEBrowserFallback();
400
- if (behavior.explainXiaoEFallback) {
511
+ if (useXiaoELogin) {
512
+ const xiaoEResult = await tryXiaoEUnifiedLogin(ctx, baseUrl, tenantKey, profileFromGlobal, behavior.allowXiaoETokenScript === true);
513
+ if (xiaoEResult)
514
+ return xiaoEResult;
515
+ }
516
+ const willOpenBrowser = !useXiaoELogin
517
+ || behavior.browserFallback !== 'desktop-only'
518
+ || shouldUseXiaoEBrowserFallback();
519
+ if (useXiaoELogin && behavior.explainXiaoEFallback) {
401
520
  ctx.logger.warn(xiaoEUnifiedLoginUnavailableMessage(baseUrl, willOpenBrowser));
402
521
  }
403
522
  if (!willOpenBrowser) {
@@ -405,6 +524,8 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
405
524
  }
406
525
  if (!baseUrl)
407
526
  throw new CliError('Missing --base-url', ExitCode.UsageError);
527
+ const agentReturnTarget = resolveAgentReturnTarget(commandOpts, ctx.env, agentType);
528
+ const agentReturnToken = agentReturnTarget ? randomBytes(24).toString('base64url') : '';
408
529
  const redirectUri = `http://127.0.0.1:${port}/callback`;
409
530
  const loginUrl = buildE10AuthorizeUrl(baseUrl, redirectUri, agentType);
410
531
  ctx.logger.info('Open browser login: ' + loginUrl);
@@ -412,6 +533,7 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
412
533
  return await new Promise((resolve, reject) => {
413
534
  let settled = false;
414
535
  let timer = null;
536
+ let pendingLoginResult = null;
415
537
  const finish = (error, result) => {
416
538
  if (settled)
417
539
  return;
@@ -433,6 +555,43 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
433
555
  };
434
556
  const server = createServer(async (req, res) => {
435
557
  const callbackUrl = new URL(req.url || '/', `http://127.0.0.1:${port}`);
558
+ if (callbackUrl.pathname === AGENT_RETURN_PATH) {
559
+ const requestToken = req.headers[AGENT_RETURN_TOKEN_HEADER];
560
+ if (req.method !== 'POST'
561
+ || !agentReturnTarget
562
+ || !pendingLoginResult
563
+ || requestToken !== agentReturnToken) {
564
+ res.writeHead(403, { 'cache-control': 'no-store', 'content-type': 'text/plain; charset=utf-8' });
565
+ res.end('Forbidden');
566
+ return;
567
+ }
568
+ const action = callbackUrl.searchParams.get('action');
569
+ if (action === 'cancel') {
570
+ const result = pendingLoginResult;
571
+ res.writeHead(204, { 'cache-control': 'no-store' });
572
+ res.end();
573
+ finish(null, result);
574
+ return;
575
+ }
576
+ if (action !== 'open') {
577
+ res.writeHead(400, { 'cache-control': 'no-store', 'content-type': 'text/plain; charset=utf-8' });
578
+ res.end('Invalid action');
579
+ return;
580
+ }
581
+ try {
582
+ await openUrlWithSystemHandler(agentReturnTarget.url);
583
+ const result = pendingLoginResult;
584
+ res.writeHead(200, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' });
585
+ res.end('{"ok":true}');
586
+ finish(null, result);
587
+ }
588
+ catch {
589
+ ctx.logger.warn(`Could not open ${agentReturnTarget.label} automatically.`);
590
+ res.writeHead(500, { 'cache-control': 'no-store', 'content-type': 'text/plain; charset=utf-8' });
591
+ res.end(`无法打开 ${agentReturnTarget.label},请确认应用已安装。`);
592
+ }
593
+ return;
594
+ }
436
595
  if (callbackUrl.pathname !== '/callback') {
437
596
  res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
438
597
  res.end('Not found');
@@ -460,9 +619,26 @@ async function runBrowserLogin(ctx, options, behavior = {}) {
460
619
  }
461
620
  try {
462
621
  const result = await oidcExchange(code, redirectUri, baseUrl, tenantKey, redirectAgentType, profileFromGlobal);
463
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
464
- res.end(renderE10LoginSuccessPage(result.userId));
465
- finish(null, result);
622
+ const responseHeaders = {
623
+ 'cache-control': 'no-store',
624
+ 'content-security-policy': agentReturnTarget
625
+ ? `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${agentReturnToken}'; connect-src 'self'`
626
+ : `default-src 'none'; style-src 'unsafe-inline'`,
627
+ 'content-type': 'text/html; charset=utf-8',
628
+ 'referrer-policy': 'no-referrer',
629
+ 'x-content-type-options': 'nosniff',
630
+ 'x-frame-options': 'DENY',
631
+ };
632
+ res.writeHead(200, responseHeaders);
633
+ res.end(renderE10LoginSuccessPage(result.userId, agentReturnTarget, agentReturnToken));
634
+ if (!agentReturnTarget) {
635
+ finish(null, result);
636
+ return;
637
+ }
638
+ pendingLoginResult = result;
639
+ if (timer)
640
+ clearTimeout(timer);
641
+ timer = setTimeout(() => finish(null, result), AGENT_RETURN_TIMEOUT_MS);
466
642
  }
467
643
  catch (exchangeError) {
468
644
  const message = exchangeError instanceof Error ? exchangeError.message : String(exchangeError);
@@ -530,6 +706,7 @@ function registerBrowserLoginCommand(auth, action, name, description, behavior =
530
706
  .option('--port <n>', 'Local OAuth callback port', '19800')
531
707
  .option('--agent_type <value>', 'Agent type sent to E10 authorize endpoint')
532
708
  .option('--agent-type <value>', 'Alias of --agent_type')
709
+ .option('--agent-return-url <url>', 'Desktop application URL linked by the login success page')
533
710
  .action(action(async (ctx, options) => {
534
711
  const result = await runBrowserLogin(ctx, options, behavior);
535
712
  ctx.output.write(result, { text: formatLoginResult(result) });
@@ -617,6 +794,7 @@ export function registerE10AuthCommands(parent, action) {
617
794
  registerBrowserLoginCommand(auth, action, 'login', 'Open browser OAuth2.0 login and save E10 auth');
618
795
  registerBrowserLoginCommand(auth, action, 'oidc', 'Alias of auth login for OAuth2.0 browser login');
619
796
  registerBrowserLoginCommand(auth, action, 'xiaoe', 'Try XiaoE unified login and explain before browser OAuth fallback', {
797
+ tryXiaoEUnifiedLogin: true,
620
798
  allowXiaoETokenScript: true,
621
799
  explainXiaoEFallback: true,
622
800
  browserFallback: 'desktop-only',
@@ -59,13 +59,22 @@ function envString(env, ...keys) {
59
59
  }
60
60
  return '';
61
61
  }
62
- function hasEnvPrefix(env, prefix) {
63
- return Object.keys(env).some((key) => key === prefix || key.startsWith(`${prefix}_`));
64
- }
65
62
  export function hasXiaoEEnv(env = process.env) {
66
- // `XIAOE_*` are the platform-injected variables; `WEAVER_XIAOE_*` are this
67
- // CLI's own host-integration variables. Either one marks a XiaoE host.
68
- return hasEnvPrefix(env, 'XIAOE') || hasEnvPrefix(env, 'WEAVER_XIAOE');
63
+ // Current-host markers win over WEAVER_AGENT_TYPE, which can be inherited
64
+ // from a previously saved E10 profile by `weaver-work-cli run`.
65
+ if (Object.keys(env).some((key) => key.startsWith('WORKBUDDY_') || key.startsWith('CODEX_'))) {
66
+ return false;
67
+ }
68
+ const explicitAgentType = envString(env, 'WORKBUDDY_AGENT_TYPE', 'CODEX_AGENT_TYPE', 'WEAVER_AGENT_TYPE');
69
+ if (explicitAgentType) {
70
+ const normalized = explicitAgentType.toLowerCase().replace(/[^a-z0-9\p{Script=Han}]+/gu, '');
71
+ return ['xiaoe', 'xiaoeclient', '小e', '智能小e'].includes(normalized);
72
+ }
73
+ // XiaoE injects this pair into the command process for the current run. A
74
+ // tokens.py path, client installation, XIAOE_SKILL_* variable, or
75
+ // WEAVER_XIAOE_* configuration alone is credential availability, not proof
76
+ // that the current agent is XiaoE.
77
+ return hasXiaoEPlatformCredential(env);
69
78
  }
70
79
  export function describeXiaoECredentialSource(source) {
71
80
  // Step 1 is the platform-injected credential (XIAOE_USER_TOKEN + XIAOE_DOMAIN):
@@ -1,9 +1,9 @@
1
1
  import { CliError, ExitCode } from '../../core/errors.js';
2
2
  import { E10AuthSession, getActiveProfile, readProfileConfig } from './auth/session.js';
3
- import { createXiaoEUnifiedLoginUnavailableError, hasXiaoEEnv, hasXiaoETokenScript, resolveXiaoEPreferredBaseUrl, shouldUseXiaoEBrowserFallback, tryXiaoEUnifiedLogin, } from './auth/xiaoe.js';
3
+ import { createXiaoEUnifiedLoginUnavailableError, hasXiaoEEnv, resolveXiaoEPreferredBaseUrl, shouldUseXiaoEBrowserFallback, tryXiaoEUnifiedLogin, } from './auth/xiaoe.js';
4
4
  import { E10ApiClient } from './client.js';
5
5
  function shouldPreferXiaoEUnifiedLogin(options, env) {
6
- return !options.auth && !options.profile && (hasXiaoEEnv(env) || hasXiaoETokenScript(env));
6
+ return !options.auth && !options.profile && hasXiaoEEnv(env);
7
7
  }
8
8
  export async function getE10Session(ctx, load = {}) {
9
9
  const options = ctx.getGlobalOptions();
@@ -27,7 +27,7 @@ export async function requireE10Session(ctx, load = {}) {
27
27
  const session = await getE10Session(ctx, load);
28
28
  if (!session) {
29
29
  const options = ctx.getGlobalOptions();
30
- if ((hasXiaoEEnv(ctx.env) || hasXiaoETokenScript(ctx.env)) && !shouldUseXiaoEBrowserFallback()) {
30
+ if (hasXiaoEEnv(ctx.env) && !shouldUseXiaoEBrowserFallback()) {
31
31
  const baseUrl = resolveXiaoEPreferredBaseUrl({
32
32
  explicitBaseUrl: options.baseUrl,
33
33
  profileBaseUrl: readProfileConfig(getActiveProfile()).baseUrl,
@@ -62,7 +62,7 @@ export class E10RequestRuntime {
62
62
  const headers = this.composeHeaders(mutable, request.trustedHeaders);
63
63
  const init = this.buildFetchInit(mutable, headers);
64
64
  const response = await this.fetchWithTimeout(url, init, mutable.timeoutMs);
65
- const parsed = await this.readResponse(response, mutable.responseType, mutable.bigIntStrings);
65
+ const parsed = await this.readResponse(response, mutable.responseType, mutable.bigIntStrings, mutable.maxResponseBytes, mutable.timeoutMs);
66
66
  await this.runAfterResponse(mutable, sessionContext, parsed);
67
67
  return parsed;
68
68
  }
@@ -82,6 +82,8 @@ export class E10RequestRuntime {
82
82
  body: request.body,
83
83
  form: request.form,
84
84
  formData: request.formData,
85
+ streamBody: request.streamBody,
86
+ maxResponseBytes: request.maxResponseBytes,
85
87
  timeoutMs: request.timeoutMs || DEFAULT_TIMEOUT_MS,
86
88
  responseType: request.responseType || 'json',
87
89
  bigIntStrings: request.bigIntStrings ?? DEFAULT_BIG_INT_STRINGS,
@@ -156,7 +158,11 @@ export class E10RequestRuntime {
156
158
  headers,
157
159
  redirect: 'manual',
158
160
  };
159
- if (request.formData) {
161
+ if (request.streamBody) {
162
+ init.body = request.streamBody;
163
+ init.duplex = 'half';
164
+ }
165
+ else if (request.formData) {
160
166
  init.body = request.formData;
161
167
  }
162
168
  else if (request.form !== undefined) {
@@ -192,7 +198,7 @@ export class E10RequestRuntime {
192
198
  clearTimeout(timer);
193
199
  }
194
200
  }
195
- async readResponse(response, responseType, bigIntStrings = DEFAULT_BIG_INT_STRINGS) {
201
+ async readResponse(response, responseType, bigIntStrings = DEFAULT_BIG_INT_STRINGS, maxResponseBytes, timeoutMs = DEFAULT_TIMEOUT_MS) {
196
202
  if (responseType === 'binary') {
197
203
  return {
198
204
  status: response.status,
@@ -201,7 +207,8 @@ export class E10RequestRuntime {
201
207
  contentType: response.headers.get('content-type') || 'application/octet-stream',
202
208
  };
203
209
  }
204
- const text = await response.text();
210
+ const text = maxResponseBytes === undefined ? await response.text()
211
+ : await readBoundedText(response, maxResponseBytes, timeoutMs);
205
212
  let value = {};
206
213
  let parseError;
207
214
  try {
@@ -304,6 +311,40 @@ function withQuery(baseUrl, requestPath, query = {}) {
304
311
  }
305
312
  return url;
306
313
  }
314
+ async function readBoundedText(response, limit, timeoutMs) {
315
+ if (!response.body)
316
+ return '';
317
+ const reader = response.body.getReader();
318
+ const chunks = [];
319
+ let size = 0;
320
+ let expired = false;
321
+ const timer = setTimeout(() => { expired = true; void reader.cancel().catch(() => undefined); }, timeoutMs);
322
+ try {
323
+ while (true) {
324
+ const next = await reader.read();
325
+ if (expired)
326
+ throw new E10ApiError('network', 'timeout', '文件服务响应读取超时', { retryable: false });
327
+ if (next.done)
328
+ break;
329
+ size += next.value.byteLength;
330
+ if (size > limit) {
331
+ await reader.cancel().catch(() => undefined);
332
+ throw new E10ApiError('network', 'response_too_large', '文件服务响应超过大小上限', { retryable: false });
333
+ }
334
+ chunks.push(next.value);
335
+ }
336
+ return Buffer.concat(chunks, size).toString('utf8');
337
+ }
338
+ catch (error) {
339
+ if (error instanceof E10ApiError)
340
+ throw error;
341
+ throw new E10ApiError('network', 'response_read_failed', '文件服务响应读取失败', { retryable: false });
342
+ }
343
+ finally {
344
+ clearTimeout(timer);
345
+ reader.releaseLock();
346
+ }
347
+ }
307
348
  function sanitizePublicHeaders(headers) {
308
349
  const next = { ...headers };
309
350
  stripReservedHeaders(next);
@@ -338,6 +379,7 @@ function requestForHook(request) {
338
379
  query: { ...request.query },
339
380
  headers: { ...request.headers },
340
381
  body: cloneForHook(request.body),
382
+ streamBody: undefined,
341
383
  };
342
384
  }
343
385
  function cloneForHook(value) {
@@ -0,0 +1,124 @@
1
+ /** Port of weaver-file-upload's local inspection/context contract. Never buffer a complete upload file. */
2
+ import { createHash } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, open, readFile } from 'node:fs/promises';
5
+ import { basename, extname, isAbsolute } from 'node:path';
6
+ import { caizhilianValidationError } from '../errors.js';
7
+ import { assertNoLossyIntegers } from '../operations/shared.js';
8
+ export const RESERVED_CONTEXT_FIELDS = ['sf_request_type', 'compressImg', 'module', 'name', 'size', 'MD5',
9
+ 'lastModified', 'lastModifiedDate', 'preUploadId', 'uploadType', 'uploadShardSize', 'chunk', 'chunks', 'file'];
10
+ const RESERVED = new Set(RESERVED_CONTEXT_FIELDS.map((key) => key.toLowerCase()));
11
+ const PRIVATE_KEYS = /^(cookie|eteamsid|eteams_id|authorization|token|access_token|password|ticket|__proto__|prototype|constructor)$/i;
12
+ const MIME = { txt: 'text/plain', pdf: 'application/pdf', png: 'image/png',
13
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
14
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
15
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
16
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' };
17
+ export const READ_FLAGS = constants.O_RDONLY | (process.platform === 'win32' ? 0 : constants.O_NOFOLLOW);
18
+ async function regularFile(value, label) {
19
+ if (typeof value !== 'string' || !isAbsolute(value) || value.includes('\0')) {
20
+ throw caizhilianValidationError('file_path_invalid', `${label} 必须是本地文件绝对路径`);
21
+ }
22
+ const info = await lstat(value).catch(() => {
23
+ throw caizhilianValidationError('file_not_found', `${label} 不存在或不可访问`);
24
+ });
25
+ if (!info.isFile() || info.isSymbolicLink()) {
26
+ throw caizhilianValidationError('file_not_regular', `${label} 必须是非符号链接的普通文件`);
27
+ }
28
+ if (!Number.isSafeInteger(info.size))
29
+ throw caizhilianValidationError('file_size_invalid', '文件大小超出支持范围');
30
+ return { path: value, info };
31
+ }
32
+ export async function inspectUploadFile(value) {
33
+ const { path, info } = await regularFile(value, 'file_path');
34
+ const md5 = createHash('md5');
35
+ const sha256 = createHash('sha256');
36
+ try {
37
+ const handle = await open(path, READ_FLAGS);
38
+ try {
39
+ for await (const bytes of handle.createReadStream({ autoClose: false })) {
40
+ md5.update(bytes);
41
+ sha256.update(bytes);
42
+ }
43
+ }
44
+ finally {
45
+ await handle.close();
46
+ }
47
+ }
48
+ catch {
49
+ throw caizhilianValidationError('file_unreadable', '上传文件不可读取');
50
+ }
51
+ const after = await lstat(path);
52
+ if (info.size !== after.size || info.mtimeMs !== after.mtimeMs || info.ctimeMs !== after.ctimeMs
53
+ || info.ino !== after.ino || after.isSymbolicLink()) {
54
+ throw caizhilianValidationError('file_changed', '文件在校验过程中发生变化,请重新准备上传');
55
+ }
56
+ const extension = extname(path).slice(1).toLowerCase();
57
+ const mimeType = MIME[extension] ?? 'application/octet-stream';
58
+ return { path, name: basename(path), extension, mime_type: mimeType, is_image: mimeType.startsWith('image/'),
59
+ size: info.size, mtime_ms: info.mtimeMs, ctime_ms: info.ctimeMs, inode: info.ino,
60
+ md5: md5.digest('hex'), sha256: sha256.digest('hex') };
61
+ }
62
+ export function assertUploadSnapshot(file, info) {
63
+ if (!info.isFile() || info.size !== file.size || info.mtimeMs !== file.mtime_ms
64
+ || info.ctimeMs !== file.ctime_ms || info.ino !== file.inode) {
65
+ throw caizhilianValidationError('file_changed', '文件在上传过程中发生变化,请停止并重新准备上传');
66
+ }
67
+ }
68
+ function validateContext(value, depth = 0) {
69
+ if (depth > 12)
70
+ throw caizhilianValidationError('context_invalid', '业务上下文嵌套不能超过 12 层');
71
+ if (value === null || typeof value !== 'object')
72
+ return;
73
+ for (const [key, item] of Object.entries(value)) {
74
+ if (PRIVATE_KEYS.test(key) || /[\r\n\0]/.test(key) || (depth === 0 && RESERVED.has(key.toLowerCase()))) {
75
+ throw caizhilianValidationError('context_forbidden_field', '业务上下文包含凭证、危险字段或协议保留字段');
76
+ }
77
+ validateContext(item, depth + 1);
78
+ }
79
+ }
80
+ export async function readUploadContext(value) {
81
+ let fields = {};
82
+ if (value !== undefined && value !== '') {
83
+ const { path, info } = await regularFile(value, 'context_file');
84
+ if (info.size > 64 * 1024)
85
+ throw caizhilianValidationError('context_too_large', '业务上下文不能超过 64 KiB');
86
+ const content = await readFile(path, { flag: READ_FLAGS });
87
+ if (content.length > 64 * 1024)
88
+ throw caizhilianValidationError('context_too_large', '业务上下文不能超过 64 KiB');
89
+ try {
90
+ fields = JSON.parse(content.toString('utf8'));
91
+ }
92
+ catch {
93
+ throw caizhilianValidationError('context_invalid', 'context_file 必须是合法 JSON 对象');
94
+ }
95
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
96
+ throw caizhilianValidationError('context_invalid', 'context_file 必须是 JSON 对象');
97
+ }
98
+ validateContext(fields);
99
+ assertNoLossyIntegers(fields, 'context_file');
100
+ }
101
+ const keys = Object.keys(fields).sort();
102
+ const ordered = Object.fromEntries(keys.map((key) => [key, fields[key]]));
103
+ return { fields: ordered, keys, sha256: createHash('sha256').update(JSON.stringify(ordered)).digest('hex') };
104
+ }
105
+ export function uploadModule(value) {
106
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_.-]{1,64}$/.test(value)) {
107
+ throw caizhilianValidationError('module_invalid', 'module 必须来自真实业务上下文,且为 1–64 个字母、数字或 ._-');
108
+ }
109
+ return value;
110
+ }
111
+ export function uploadChatId(value) {
112
+ const id = value ?? 'cli';
113
+ if (typeof id !== 'string' || !/^[A-Za-z0-9_.-]{1,128}$/.test(id) || id === '.' || id === '..') {
114
+ throw caizhilianValidationError('chat_id_invalid', 'chat_id 必须为 1–128 个字母、数字或 ._-');
115
+ }
116
+ return id;
117
+ }
118
+ export function uploadTimeout(value) {
119
+ const seconds = value ?? 300;
120
+ if (typeof seconds !== 'number' || !Number.isInteger(seconds) || seconds < 1 || seconds > 3600) {
121
+ throw caizhilianValidationError('timeout_invalid', 'timeout_seconds 必须为 1–3600 的整数');
122
+ }
123
+ return seconds * 1000;
124
+ }