weaver-work-cli 0.1.22 → 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 (57) 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/meeting/operations/change.js +7 -4
  16. package/dist/shortcuts/meeting/operations/create.js +5 -3
  17. package/dist/shortcuts/meeting/operations/eb/decision.js +22 -2
  18. package/dist/shortcuts/meeting/operations/lifecycle.js +8 -6
  19. package/dist/shortcuts/meeting/operations/receipts.js +54 -21
  20. package/dist/shortcuts/meeting/operations/shared.js +28 -0
  21. package/dist/shortcuts/meeting/operations/signs.js +14 -4
  22. package/dist/shortcuts/project/operations/read.js +7 -6
  23. package/dist/shortcuts/workflow/continuation.js +14 -9
  24. package/dist/shortcuts/workflow/endpoints.js +11 -1
  25. package/dist/shortcuts/workflow/operations/flow-status.js +63 -0
  26. package/dist/shortcuts/workflow/operations/form.js +650 -518
  27. package/dist/shortcuts/workflow/operations/read.js +374 -13
  28. package/dist/shortcuts/workflow/operations/shared.js +3 -0
  29. package/dist/shortcuts/workflow/operations/write.js +5 -5
  30. package/dist/shortcuts/workflow/operations.js +8 -6
  31. package/docs/caizhilian.md +8 -2
  32. package/docs/e10-auth.md +18 -3
  33. package/package.json +1 -1
  34. package/skills/weaver-e10-caizhilian/SKILL.md +8 -3
  35. package/skills/weaver-e10-caizhilian/references/file-upload.md +100 -295
  36. package/skills/weaver-e10-caizhilian/references/folder.md +31 -3
  37. package/skills/weaver-e10-caizhilian/references/source-manifest.json +536 -25
  38. package/skills/weaver-e10-jiuchuanhui/SKILL.md +9 -1
  39. package/skills/weaver-e10-meeting/SKILL.md +3 -2
  40. package/skills/weaver-e10-meeting/references/meeting-eb.md +10 -0
  41. package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +7 -2
  42. package/skills/weaver-e10-meeting/references/meeting-verification.md +1 -1
  43. package/skills/weaver-e10-meeting.zip +0 -0
  44. package/skills/weaver-e10-shared/SKILL.md +12 -2
  45. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +21 -3
  46. package/skills/weaver-e10-shijingran/SKILL.md +68 -3
  47. package/skills/weaver-e10-shijingran/references/disabled-capabilities.md +22 -0
  48. package/skills/weaver-e10-shijingran/references/source-manifest.json +12 -4
  49. package/skills/weaver-e10-workflow/SKILL.md +10 -8
  50. package/skills/weaver-e10-workflow/references/source-manifest.json +36 -31
  51. package/skills/weaver-e10-workflow/references/workflow-batch.md +2 -4
  52. package/skills/weaver-e10-workflow/references/workflow-create.md +58 -36
  53. package/skills/weaver-e10-workflow/references/workflow-edit.md +2 -1
  54. package/skills/weaver-e10-workflow/references/workflow-memory.md +2 -2
  55. package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -1
  56. package/skills/weaver-e10-workflow/references/workflow-query.md +9 -7
  57. package/skills/weaver-e10-workflow/references/workflow-share.md +2 -0
@@ -0,0 +1,47 @@
1
+ /** Exact-length multipart body with bounded file slices and RFC 5987 UTF-8 filenames. */
2
+ import { randomBytes } from 'node:crypto';
3
+ import { open } from 'node:fs/promises';
4
+ import { Readable } from 'node:stream';
5
+ import { assertUploadSnapshot, READ_FLAGS } from './metadata.js';
6
+ function quoted(value) { return value.replace(/[\r\n\0]/g, '_').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
7
+ function encodedFilename(value) {
8
+ return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
9
+ }
10
+ export function uploadMultipart(fields, file) {
11
+ const boundary = `weaver-${randomBytes(16).toString('hex')}`;
12
+ const parts = [];
13
+ for (const [key, value] of Object.entries(fields)) {
14
+ const text = value === null || value === undefined ? '' : typeof value === 'string' ? value : JSON.stringify(value);
15
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${quoted(key)}"\r\n\r\n${text}\r\n`));
16
+ }
17
+ if (file) {
18
+ const fallback = quoted(file.meta.name.replace(/[^\x20-\x7e]/g, '_'));
19
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fallback}"; `
20
+ + `filename*=UTF-8''${encodedFilename(file.meta.name)}\r\nContent-Type: ${file.meta.mime_type}\r\n\r\n`));
21
+ }
22
+ const tail = Buffer.from(`${file ? '\r\n' : ''}--${boundary}--\r\n`);
23
+ const body = Readable.from((async function* () {
24
+ yield* parts;
25
+ if (file && file.size > 0) {
26
+ const handle = await open(file.meta.path, READ_FLAGS);
27
+ try {
28
+ assertUploadSnapshot(file.meta, await handle.stat());
29
+ const stream = handle.createReadStream({ start: file.start, end: file.start + file.size - 1, autoClose: false });
30
+ try {
31
+ for await (const bytes of stream)
32
+ yield bytes;
33
+ assertUploadSnapshot(file.meta, await handle.stat());
34
+ }
35
+ finally {
36
+ stream.destroy();
37
+ }
38
+ }
39
+ finally {
40
+ await handle.close();
41
+ }
42
+ }
43
+ yield tail;
44
+ })(), { objectMode: false });
45
+ return { body, contentType: `multipart/form-data; boundary=${boundary}`,
46
+ contentLength: parts.reduce((total, bytes) => total + bytes.length, tail.length + (file?.size ?? 0)) };
47
+ }
@@ -0,0 +1,116 @@
1
+ /** Upload strategy owner, adapted from weaver-file-upload to the CLI-owned authenticated host. */
2
+ import { CaizhilianCliError, caizhilianValidationError } from '../errors.js';
3
+ import { ExitCode } from '../../../core/errors.js';
4
+ import { uploadMultipart } from './multipart.js';
5
+ const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
6
+ const invalid = (subtype, message) => { throw caizhilianValidationError(subtype, message); };
7
+ function checkedData(response) {
8
+ const envelope = object(response);
9
+ const data = object(envelope.data);
10
+ const code = envelope.code === undefined || envelope.code === null ? '' : String(envelope.code).toLowerCase();
11
+ if (code && !['0', '200', 'success'].includes(code))
12
+ invalid('upload_business_failed', '文件服务未确认操作成功');
13
+ for (const record of [envelope, data]) {
14
+ if (record.status === false || record.success === false || record.fail === true) {
15
+ invalid('upload_business_failed', '文件服务未确认操作成功');
16
+ }
17
+ }
18
+ if (!envelope.data || typeof envelope.data !== 'object' || Array.isArray(envelope.data)) {
19
+ invalid('upload_response_invalid', '文件服务响应缺少有效数据对象');
20
+ }
21
+ return data;
22
+ }
23
+ function fileRecord(data) {
24
+ const candidates = [[data], [object(data.fileObj)], [object(data.uploadModuleDto)],
25
+ [object(data.uploadModuleDto?.fileObj), object(data.uploadModuleDto)], [object(data.imUploadInfoDto)],
26
+ [object(data.imUploadInfoDto?.fileObj), object(data.imUploadInfoDto)]];
27
+ for (const [record, parent] of candidates) {
28
+ const id = record.fileId ?? record.fileid;
29
+ if (typeof id === 'number' && !Number.isSafeInteger(id))
30
+ invalid('file_id_invalid', '文件 ID 精度不可靠');
31
+ if ((typeof id === 'string' || typeof id === 'number') && String(id).trim()) {
32
+ return { id: String(id).trim(), sizes: [record.size, parent?.size, data.size] };
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+ function terminal(data, file) {
38
+ const record = fileRecord(data);
39
+ if (!record || data.endChunk === false)
40
+ invalid('upload_not_terminal', '上传未返回已完成的非空文件 ID');
41
+ for (const reported of record.sizes) {
42
+ if (reported === undefined || reported === null || reported === '')
43
+ continue;
44
+ const size = Number(reported);
45
+ if (!Number.isSafeInteger(size) || size < 0 || (size !== 0 && size !== file.size)) {
46
+ invalid('upload_size_mismatch', '服务端文件大小与本地文件不一致');
47
+ }
48
+ }
49
+ return record.id;
50
+ }
51
+ async function send(host, path, fields, timeoutMs, file) {
52
+ const multipart = uploadMultipart(fields, file);
53
+ try {
54
+ return checkedData(await host.request({ method: 'POST', path, query: { sf_request_type: 'ajax' },
55
+ streamMultipart: multipart, timeoutMs, maxResponseBytes: 4 * 1024 * 1024, requireHttp200: true,
56
+ tolerateBusinessError: true,
57
+ operation: 'caizhilian.file-upload.upload.apply' }));
58
+ }
59
+ catch (error) {
60
+ const failure = error;
61
+ if (file && (failure?.type === 'network' || failure?.subtype === 'http_error')) {
62
+ throw new CaizhilianCliError('partial', 'write_uncertain', '上传请求已发出,但未确认结果;请勿自动重复上传', {
63
+ retryable: false, details: { execution_state: 'unknown', retry_safe: false }, exitCode: ExitCode.Partial,
64
+ });
65
+ }
66
+ throw error;
67
+ }
68
+ finally {
69
+ multipart.body.destroy();
70
+ }
71
+ }
72
+ export async function uploadToFileService(host, file, module, context, timeoutMs) {
73
+ // Node stat retains fractional milliseconds; E10 parses these fields as integer milliseconds.
74
+ const lastModified = String(Math.floor(file.mtime_ms));
75
+ const fields = { ...context.fields, compressImg: 'false', module, name: file.name, size: String(file.size),
76
+ MD5: file.md5, lastModified, lastModifiedDate: lastModified };
77
+ const pre = await send(host, '/api/file/preUploadCheck', fields, timeoutMs);
78
+ const type = pre.uploadType;
79
+ if (!['second', 'common', 'chunk'].includes(type))
80
+ invalid('upload_type_invalid', '文件服务返回未知上传策略');
81
+ if (type === 'second')
82
+ return { file_id: terminal(pre, file), upload_type: type };
83
+ if (typeof pre.preUploadId !== 'string' || !pre.preUploadId.trim()) {
84
+ // E10 may return preUploadId as a safe numeric ID.
85
+ if (typeof pre.preUploadId !== 'number' || !Number.isSafeInteger(pre.preUploadId) || pre.preUploadId <= 0) {
86
+ invalid('upload_precheck_invalid', '预上传检查未返回有效 preUploadId');
87
+ }
88
+ }
89
+ const uploadFields = { ...fields, preUploadId: String(pre.preUploadId), uploadType: type };
90
+ if (type === 'common') {
91
+ const data = await send(host, '/api/file/module/upload', uploadFields, timeoutMs, { meta: file, start: 0, size: file.size });
92
+ return { file_id: terminal(data, file), upload_type: type };
93
+ }
94
+ const chunkMiB = Number(pre.chunkDto?.chunkSize);
95
+ if (!Number.isSafeInteger(chunkMiB) || chunkMiB <= 0 || file.size <= 0) {
96
+ invalid('chunk_size_invalid', '分片上传需要非空文件和正整数 MiB 分片大小');
97
+ }
98
+ const chunkSize = chunkMiB * 1024 * 1024;
99
+ if (!Number.isSafeInteger(chunkSize) || chunkSize <= 0)
100
+ invalid('chunk_size_invalid', '分片大小必须是有效的正 MiB 值');
101
+ const count = Math.max(1, Math.ceil(file.size / chunkSize));
102
+ if (pre.chunkDto?.chunkCount !== undefined && Number(pre.chunkDto.chunkCount) !== count) {
103
+ invalid('chunk_count_mismatch', '服务端分片数量与本地计算不一致');
104
+ }
105
+ for (let index = 0; index < count; index++) {
106
+ const start = index * chunkSize;
107
+ const size = Math.min(chunkSize, file.size - start);
108
+ const data = await send(host, '/api/file/module/upload', { ...uploadFields, chunk: index, chunks: count,
109
+ uploadShardSize: String(size) }, timeoutMs, { meta: file, start, size });
110
+ if (index === count - 1)
111
+ return { file_id: terminal(data, file), upload_type: 'chunk' };
112
+ if (data.endChunk !== false || fileRecord(data))
113
+ invalid('chunk_terminal_early', '中间分片缺少明确未完成状态或提前返回文件 ID');
114
+ }
115
+ return invalid('upload_not_terminal', '分片上传未确认完成');
116
+ }
@@ -0,0 +1,37 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { CaizhilianCliError } from '../errors.js';
6
+ import { ExitCode } from '../../../core/errors.js';
7
+ export async function writeUploadReceipt(env, chatId, module, file, context, verified) {
8
+ const session = await env.host.getSession();
9
+ const identity = JSON.stringify({ baseUrl: session.baseUrl, userId: session.userId, tenantKey: session.tenantKey });
10
+ const result = { version: 'weaver_file_upload_result.v1', status: 'completed', business_verified: true,
11
+ upload_type: verified.upload_type, file: { file_id: verified.file_id, name: file.name, size: file.size,
12
+ mime_type: file.mime_type, extension: file.extension, is_image: file.is_image },
13
+ integrity: { md5: file.md5, sha256: file.sha256 },
14
+ request_evidence: { module, context_keys: context.keys, context_sha256: context.sha256,
15
+ environment_identity_sha256: createHash('sha256').update(identity).digest('hex'),
16
+ login_mode: 'weaver-work-cli', automatic_fallback: false } };
17
+ const configPath = env.ctx.config?.path();
18
+ const stateRoot = configPath ? dirname(configPath) : env.ctx.env?.WEAVER_WORK_CLI_HOME
19
+ || env.ctx.env?.WORK_CLI_HOME || resolve(homedir(), '.weaver-work-cli');
20
+ const directory = resolve(stateRoot, 'caizhilian', 'file-upload', 'runs', chatId, 'receipts');
21
+ const filename = resolve(directory, `upload-${randomUUID()}.json`);
22
+ const content = `${JSON.stringify(result, null, 2)}\n`;
23
+ try {
24
+ await mkdir(directory, { recursive: true, mode: 0o700 });
25
+ if (process.platform !== 'win32')
26
+ await chmod(directory, 0o700);
27
+ await writeFile(filename, content, { flag: 'wx', mode: 0o600 });
28
+ }
29
+ catch {
30
+ throw new CaizhilianCliError('partial', 'receipt_write_failed', '文件已上传,但本地回执保存失败;请勿重复上传', {
31
+ retryable: false, partialData: { file_id: verified.file_id, upload_type: verified.upload_type },
32
+ details: { execution_state: 'completed', retry_safe: false }, exitCode: ExitCode.Partial,
33
+ });
34
+ }
35
+ return { ...result, file_id: verified.file_id, file_name: file.name, size: file.size, ext: file.extension,
36
+ receipt_ref: filename, receipt_sha256: createHash('sha256').update(content).digest('hex') };
37
+ }
@@ -110,9 +110,13 @@ export function createCaizhilianHost(ctx, dependencies = {}) {
110
110
  });
111
111
  }
112
112
  async function doRequest(request, binary) {
113
- const { method, path, query, body, operation, form, multipart, headers: extraHeaders = {}, timeoutMs = CAIZHILAN_TIMEOUT_MS, } = request;
113
+ const { method, path, query, body, operation, form, multipart, streamMultipart, maxResponseBytes, headers: extraHeaders = {}, timeoutMs = CAIZHILAN_TIMEOUT_MS, } = request;
114
114
  assertCaizhilianPath(path);
115
- const resolvedMethod = method || (body === undefined && form === undefined && multipart === undefined ? 'GET' : 'POST');
115
+ const resolvedMethod = method || (body === undefined && form === undefined && multipart === undefined
116
+ && streamMultipart === undefined ? 'GET' : 'POST');
117
+ const headers = { 'X-Requested-With': 'XMLHttpRequest', ...extraHeaders, ...(streamMultipart ? {
118
+ 'Content-Type': streamMultipart.contentType, 'Content-Length': String(streamMultipart.contentLength),
119
+ } : {}) };
116
120
  const rt = await runtime();
117
121
  const response = binary
118
122
  ? await rt.requestBinary({
@@ -124,7 +128,9 @@ export function createCaizhilianHost(ctx, dependencies = {}) {
124
128
  body: multipart === undefined ? body : undefined,
125
129
  form: multipart === undefined && form === undefined ? undefined : formBody(form ?? {}),
126
130
  formData: multipart,
127
- headers: { 'X-Requested-With': 'XMLHttpRequest', ...extraHeaders },
131
+ streamBody: streamMultipart?.body,
132
+ maxResponseBytes,
133
+ headers,
128
134
  timeoutMs,
129
135
  })
130
136
  : await rt.requestJson({
@@ -136,7 +142,9 @@ export function createCaizhilianHost(ctx, dependencies = {}) {
136
142
  body: multipart === undefined ? body : undefined,
137
143
  form: multipart === undefined && form === undefined ? undefined : formBody(form ?? {}),
138
144
  formData: multipart,
139
- headers: { 'X-Requested-With': 'XMLHttpRequest', ...extraHeaders },
145
+ streamBody: streamMultipart?.body,
146
+ maxResponseBytes,
147
+ headers,
140
148
  timeoutMs,
141
149
  });
142
150
  return response;
@@ -156,7 +164,7 @@ export function createCaizhilianHost(ctx, dependencies = {}) {
156
164
  }
157
165
  throw caizhilianNetworkError('protocol', `接口返回不是合法 JSON(HTTP ${response.status})`, { retryable: false });
158
166
  }
159
- if (response.status < 200 || response.status >= 300) {
167
+ if (response.status < 200 || response.status >= 300 || (request.requireHttp200 && response.status !== 200)) {
160
168
  throw new CaizhilianCliError('api', 'http_error', `采知连接口 HTTP ${response.status}`, {
161
169
  code: response.status,
162
170
  details: { status: response.status, message: caizhilianBusinessMessage(response.value) },
@@ -1,192 +1,85 @@
1
- /**
2
- * caizhilian.file-upload — 文件上传(本地文件上传到文件服务)
3
- *
4
- * 覆盖本地文件上传到 E10 文件服务的完整流程:
5
- * - contract: 获取上传接口合约
6
- * - prepare: 本地文件校验与元数据准备
7
- * - upload: 上传文件并获取 fileId
8
- *
9
- * 接口:
10
- * - 预检查:POST /api/file/preUploadCheck?sf_request_type=ajax
11
- * - 上传:POST /api/file/module/upload?sf_request_type=ajax
12
- */
13
- import { CaizhilianCliError, caizhilianValidationError } from '../../errors.js';
14
- import { caizhilianSuccess } from '../types.js';
15
- import { requireNonEmpty } from '../shared.js';
16
- import { createHash } from 'node:crypto';
17
- import { readFileSync, statSync } from 'node:fs';
18
- import { basename, extname } from 'node:path';
19
- const ACTION = 'caizhilian.file-upload';
20
- // ── 接口路径 ──────────────────────────────────────────────
21
- const PRE_UPLOAD_CHECK_PATH = '/api/file/preUploadCheck?sf_request_type=ajax';
22
- const UPLOAD_PATH = '/api/file/module/upload?sf_request_type=ajax';
23
- // ── 辅助函数 ──────────────────────────────────────────────
24
- function asRecord(value) {
25
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1
+ /** Weaver file upload integrated with the existing CLI confirmation and authentication boundary. */
2
+ import { caizhilianConfirmationError, caizhilianValidationError } from '../../errors.js';
3
+ import { inspectUploadFile, readUploadContext, RESERVED_CONTEXT_FIELDS, uploadChatId, uploadModule, uploadTimeout } from '../../file-upload/metadata.js';
4
+ import { uploadToFileService } from '../../file-upload/protocol.js';
5
+ import { writeUploadReceipt } from '../../file-upload/receipt.js';
6
+ import { issueWriteLease } from '../confirm.js';
7
+ import { caizhilianPrepared, caizhilianSuccess } from '../types.js';
8
+ const PREFIX = 'caizhilian.file-upload';
9
+ const COMMAND = 'weaver-work-cli --json caizhilian run';
10
+ const CONTEXT_PURPOSES = {
11
+ source: '上传来源场景', fieldId: '附件字段或控件 ID', sourceRefId: '来源业务对象引用 ID',
12
+ authModule: '服务端权限校验模块', customParam: '模块扩展参数', refId: '最终关联对象 ID',
13
+ commonText: '通用场景参数', folderId: '目标文件夹 ID',
14
+ };
15
+ async function getContract() {
16
+ return caizhilianSuccess(`${PREFIX}.contract`, { contract: {
17
+ version: 'agent_callable_interface_contract.v1', authentication: 'weaver-work-cli', automatic_fallback: false,
18
+ upload_strategies: ['second', 'common', 'chunk'],
19
+ actions: ['contract', 'prepare', 'upload.prepare', 'upload.apply'],
20
+ canonical_commands: ['contract', 'prepare', 'upload.prepare', 'upload.apply'].map((name) => `${COMMAND} ${PREFIX}.${name}${name === 'contract' ? '' : ' --input-json <businessInputJson>'}`
21
+ + (name === 'upload.apply' ? ' --continuation <token> --yes' : '')),
22
+ required: { prepare: ['file_path', 'module'], 'upload.prepare': ['file_path', 'module', 'user_confirmed'],
23
+ 'upload.apply': ['file_path', 'module', 'user_confirmed', 'continuation', 'confirm'] },
24
+ optional: { prepare: ['context_file', 'chat_id', 'timeout_seconds'],
25
+ 'upload.prepare': ['context_file', 'chat_id', 'timeout_seconds'],
26
+ 'upload.apply': ['context_file', 'chat_id', 'timeout_seconds'] },
27
+ user_input_guide: { required: [
28
+ { name: 'file_path', purpose: '需要上传的本地普通文件绝对路径,直接复用宿主附件路径' },
29
+ { name: 'module', purpose: '真实业务模块;不得猜测或使用默认模块' },
30
+ ], conditionally_required: [], optional: [
31
+ { name: 'context_file', purpose: '可选业务上下文 JSON 文件绝对路径,最多 64 KiB' },
32
+ { name: 'chat_id', purpose: '隔离对话回执;默认 cli,由宿主注入' },
33
+ { name: 'timeout_seconds', purpose: '上传请求及响应读取超时(1–3600 秒,默认 300)' },
34
+ ...Object.entries(CONTEXT_PURPOSES).map(([name, purpose]) => ({ name: `context_file.${name}`, purpose })),
35
+ ] }, reserved_context_fields: RESERVED_CONTEXT_FIELDS,
36
+ success_evidence: ['business_verified', 'file.file_id', 'receipt_ref', 'receipt_sha256'],
37
+ login: '登录由 weaver-work-cli auth 托管,本模块不读取其他 Skill 登录态或启动独立浏览器',
38
+ } });
26
39
  }
27
- function isPlainObject(value) {
28
- return typeof value === 'object' && value !== null && !Array.isArray(value);
40
+ function requireUploadAuthorization(args) {
41
+ if (args.user_confirmed !== true) {
42
+ throw caizhilianConfirmationError(`${PREFIX}.upload`, { hint: '上传必须来自当前用户明确授权,并传 user_confirmed=true' });
43
+ }
29
44
  }
30
- function md5File(filePath) {
31
- const content = readFileSync(filePath);
32
- return createHash('md5').update(content).digest('hex');
45
+ async function inspect(args) {
46
+ const module = uploadModule(args.module);
47
+ const chatId = uploadChatId(args.chat_id);
48
+ const timeoutMs = uploadTimeout(args.timeout_seconds);
49
+ const context = await readUploadContext(args.context_file);
50
+ const file = await inspectUploadFile(args.file_path);
51
+ return { file, context, module, chatId, timeoutMs };
33
52
  }
34
- function sha256File(filePath) {
35
- const content = readFileSync(filePath);
36
- return createHash('sha256').update(content).digest('hex');
53
+ function metadataPreview(prepared) {
54
+ const { file, context, module, chatId } = prepared;
55
+ return { file_path: file.path, file_name: file.name, ext: file.extension, size: file.size,
56
+ md5: file.md5, sha256: file.sha256, module, chat_id: chatId, context_keys: context.keys,
57
+ context_sha256: context.sha256 };
37
58
  }
38
- // ── 获取上传合约 ──────────────────────────────────────────
39
- async function getContract(args, env) {
40
- return caizhilianSuccess(ACTION, {
41
- contract: {
42
- version: 'agent_callable_interface_contract.v1',
43
- actions: ['contract', 'prepare', 'upload'],
44
- required: {
45
- prepare: ['file_path', 'module'],
46
- upload: ['file_path', 'module', 'user_confirmed'],
47
- },
48
- optional: {
49
- prepare: ['context_file'],
50
- upload: ['context_file', 'login_mode'],
51
- },
52
- },
53
- });
59
+ async function prepareLocal(args) {
60
+ return caizhilianSuccess(`${PREFIX}.prepare`, metadataPreview(await inspect(args)));
54
61
  }
55
- // ── 准备上传(prepare/apply)──────────────────────────────────────
56
62
  async function prepareUpload(args, env) {
57
- const filePath = requireNonEmpty(args.file_path, 'file_path');
58
- const module = requireNonEmpty(args.module, 'module');
59
- // 校验文件存在
60
- let stat;
61
- try {
62
- stat = statSync(filePath);
63
- }
64
- catch (error) {
65
- throw caizhilianValidationError('file_not_found', `文件不存在: ${filePath}`);
66
- }
67
- if (!stat.isFile()) {
68
- throw caizhilianValidationError('file_not_regular', `不是普通文件: ${filePath}`);
69
- }
70
- // 计算元数据
71
- const fileName = basename(filePath);
72
- const ext = extname(filePath).slice(1).toLowerCase();
73
- const size = stat.size;
74
- const md5 = md5File(filePath);
75
- const sha256 = sha256File(filePath);
76
- // 签发 continuation
77
- const continuation = await env.continuation.issue({
78
- operation: 'caizhilian.file-upload.upload.apply',
79
- filePath,
80
- module,
81
- fileName,
82
- ext,
83
- size,
84
- md5,
85
- sha256,
86
- });
87
- return caizhilianSuccess('caizhilian.file-upload.prepare', {
88
- file_path: filePath,
89
- file_name: fileName,
90
- ext,
91
- size,
92
- md5,
93
- sha256,
94
- module,
95
- continuation,
96
- });
63
+ const prepared = await inspect(args);
64
+ const lease = await issueWriteLease(env, `${PREFIX}.upload.apply`, args, { file_path: prepared.file.path,
65
+ module: prepared.module, file: prepared.file, context_sha256: prepared.context.sha256 });
66
+ return caizhilianPrepared(`${PREFIX}.upload.prepare`, metadataPreview(prepared), lease);
97
67
  }
98
68
  async function uploadApply(args, env, lease) {
99
- const filePath = lease.payload?.filePath;
100
- const module = lease.payload?.module;
101
- const fileName = lease.payload?.fileName;
102
- const ext = lease.payload?.ext;
103
- const size = lease.payload?.size;
104
- const md5 = lease.payload?.md5;
105
- const sha256 = lease.payload?.sha256;
106
- // 预检查
107
- const preCheckResponse = await env.host.request({
108
- method: 'POST',
109
- path: PRE_UPLOAD_CHECK_PATH,
110
- body: {
111
- fileName,
112
- fileSize: size,
113
- module,
114
- md5,
115
- },
116
- operation: 'caizhilian.file-upload.upload.apply',
117
- });
118
- const preCheckData = asRecord(preCheckResponse);
119
- if (!preCheckData.status && !preCheckData.success) {
120
- throw new CaizhilianCliError('api', 'business_error', String(preCheckData.msg || preCheckData.message || '预检查失败'), {
121
- code: preCheckData.code ?? preCheckData.status,
122
- details: { action: 'caizhilian.file-upload.upload.apply', message: String(preCheckData.msg || preCheckData.message || '预检查失败') },
123
- exitCode: 1,
124
- });
125
- }
126
- const preCheckResult = asRecord(preCheckData.data);
127
- // 判断上传策略
128
- const uploadType = preCheckResult.uploadType || 'common';
129
- const preUploadId = preCheckResult.preUploadId;
130
- // 如果是 second 策略,直接返回已有 file ID
131
- if (uploadType === 'second') {
132
- const fileId = preCheckResult.fileId || preCheckResult.fileid || preCheckResult.uploadModuleDto?.fileObj?.fileId;
133
- if (!fileId) {
134
- throw new CaizhilianCliError('api', 'business_error', 'second 策略未返回 file ID', {
135
- details: { action: 'caizhilian.file-upload.upload.apply' },
136
- exitCode: 1,
137
- });
138
- }
139
- return caizhilianSuccess('caizhilian.file-upload.upload.apply', {
140
- file_id: String(fileId),
141
- file_name: fileName,
142
- size,
143
- ext,
144
- upload_type: uploadType,
145
- });
146
- }
147
- // common/chunk 策略:上传文件
148
- const fileContent = readFileSync(filePath);
149
- const formData = new FormData();
150
- formData.append('file', new Blob([fileContent]), fileName);
151
- formData.append('preUploadId', preUploadId || '');
152
- formData.append('module', module);
153
- const uploadResponse = await env.host.request({
154
- method: 'POST',
155
- path: UPLOAD_PATH,
156
- body: formData,
157
- operation: 'caizhilian.file-upload.upload.apply',
158
- });
159
- const uploadData = asRecord(uploadResponse);
160
- if (!uploadData.status && !uploadData.success) {
161
- throw new CaizhilianCliError('api', 'business_error', String(uploadData.msg || uploadData.message || '上传失败'), {
162
- code: uploadData.code ?? uploadData.status,
163
- details: { action: 'caizhilian.file-upload.upload.apply', message: String(uploadData.msg || uploadData.message || '上传失败') },
164
- exitCode: 1,
165
- });
69
+ const prepared = await inspect(args);
70
+ if (prepared.file.sha256 !== lease.file?.sha256 || prepared.file.size !== lease.file?.size) {
71
+ throw caizhilianValidationError('file_changed', '文件与已确认内容不一致,请重新准备上传');
166
72
  }
167
- const uploadResult = asRecord(uploadData.data);
168
- const fileId = uploadResult.fileId || uploadResult.fileid || '';
169
- if (!fileId) {
170
- throw new CaizhilianCliError('api', 'business_error', '上传未返回 file ID', {
171
- details: { action: 'caizhilian.file-upload.upload.apply' },
172
- exitCode: 1,
173
- });
73
+ if (prepared.context.sha256 !== lease.context_sha256) {
74
+ throw caizhilianValidationError('context_changed', '业务上下文与已确认内容不一致,请重新准备上传');
174
75
  }
175
- return caizhilianSuccess('caizhilian.file-upload.upload.apply', {
176
- file_id: String(fileId),
177
- file_name: fileName,
178
- size,
179
- ext,
180
- upload_type: uploadType,
181
- });
76
+ const verified = await uploadToFileService(env.host, prepared.file, prepared.module, prepared.context, prepared.timeoutMs);
77
+ const result = await writeUploadReceipt(env, prepared.chatId, prepared.module, prepared.file, prepared.context, verified);
78
+ return caizhilianSuccess(`${PREFIX}.upload.apply`, result);
182
79
  }
183
- // ── 处理器汇总 ────────────────────────────────────────────
184
80
  export const caizhilianFileUploadHandlers = {
185
- 'caizhilian.file-upload.contract': { execute: getContract },
186
- 'caizhilian.file-upload.prepare': { execute: prepareUpload },
187
- 'caizhilian.file-upload.upload': {
188
- prepare: prepareUpload,
189
- apply: uploadApply,
190
- targetKeys: ['file_path', 'module'],
191
- },
81
+ [`${PREFIX}.contract`]: { execute: getContract },
82
+ [`${PREFIX}.prepare`]: { execute: prepareLocal },
83
+ [`${PREFIX}.upload`]: { validateInput: requireUploadAuthorization, prepare: prepareUpload,
84
+ apply: uploadApply, targetKeys: ['file_path', 'module'] },
192
85
  };