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.
- package/README.md +23 -2
- package/dist/internal/e10/auth/commands.js +194 -16
- package/dist/internal/e10/auth/xiaoe.js +15 -6
- package/dist/internal/e10/context.js +3 -3
- package/dist/internal/e10/request-runtime.js +46 -4
- package/dist/shortcuts/caizhilian/file-upload/metadata.js +124 -0
- package/dist/shortcuts/caizhilian/file-upload/multipart.js +47 -0
- package/dist/shortcuts/caizhilian/file-upload/protocol.js +116 -0
- package/dist/shortcuts/caizhilian/file-upload/receipt.js +37 -0
- package/dist/shortcuts/caizhilian/host.js +13 -5
- package/dist/shortcuts/caizhilian/operations/handlers/file-upload.js +72 -179
- package/dist/shortcuts/caizhilian/operations/handlers/folder.js +701 -4
- package/dist/shortcuts/caizhilian/operations/registry.js +5 -1
- package/dist/shortcuts/caizhilian/operations/specs.js +52 -0
- package/dist/shortcuts/workflow/continuation.js +14 -9
- package/dist/shortcuts/workflow/endpoints.js +11 -1
- package/dist/shortcuts/workflow/operations/flow-status.js +63 -0
- package/dist/shortcuts/workflow/operations/form.js +650 -518
- package/dist/shortcuts/workflow/operations/read.js +374 -13
- package/dist/shortcuts/workflow/operations/shared.js +3 -0
- package/dist/shortcuts/workflow/operations/write.js +5 -5
- package/dist/shortcuts/workflow/operations.js +8 -6
- package/docs/caizhilian.md +8 -2
- package/docs/e10-auth.md +18 -3
- package/package.json +1 -1
- package/skills/weaver-e10-caizhilian/SKILL.md +8 -3
- package/skills/weaver-e10-caizhilian/references/file-upload.md +100 -295
- package/skills/weaver-e10-caizhilian/references/folder.md +31 -3
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +536 -25
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +9 -1
- package/skills/weaver-e10-shared/SKILL.md +12 -2
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +21 -3
- package/skills/weaver-e10-workflow/SKILL.md +10 -8
- package/skills/weaver-e10-workflow/references/source-manifest.json +36 -31
- package/skills/weaver-e10-workflow/references/workflow-batch.md +2 -4
- package/skills/weaver-e10-workflow/references/workflow-create.md +58 -36
- package/skills/weaver-e10-workflow/references/workflow-edit.md +2 -1
- package/skills/weaver-e10-workflow/references/workflow-memory.md +2 -2
- package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -1
- package/skills/weaver-e10-workflow/references/workflow-query.md +9 -7
- 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
|
-
|
|
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
|
-
|
|
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, '"')
|
|
161
252
|
.replace(/'/g, ''');
|
|
162
253
|
}
|
|
163
|
-
export function renderE10LoginSuccessPage(userId) {
|
|
164
|
-
|
|
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
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
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,
|
|
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 &&
|
|
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 (
|
|
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.
|
|
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
|
+
}
|