weaver-work-cli 0.1.4 → 0.1.5
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/dist/internal/e10/request-runtime.js +3 -2
- package/dist/shortcuts/index.js +4 -0
- package/dist/shortcuts/meeting/continuation.js +70 -0
- package/dist/shortcuts/meeting/errors.js +63 -0
- package/dist/shortcuts/meeting/host.js +167 -0
- package/dist/shortcuts/meeting/index.js +123 -0
- package/dist/shortcuts/meeting/manifest.js +64 -0
- package/dist/shortcuts/meeting/operations/catalogs.js +58 -0
- package/dist/shortcuts/meeting/operations/change.js +242 -0
- package/dist/shortcuts/meeting/operations/conflicts.js +142 -0
- package/dist/shortcuts/meeting/operations/create.js +349 -0
- package/dist/shortcuts/meeting/operations/env.js +191 -0
- package/dist/shortcuts/meeting/operations/lifecycle.js +139 -0
- package/dist/shortcuts/meeting/operations/meetings.js +73 -0
- package/dist/shortcuts/meeting/operations/receipts.js +195 -0
- package/dist/shortcuts/meeting/operations/registry.js +47 -0
- package/dist/shortcuts/meeting/operations/rooms.js +132 -0
- package/dist/shortcuts/meeting/operations/shared.js +112 -0
- package/dist/shortcuts/meeting/operations/signs.js +41 -0
- package/dist/shortcuts/meeting/operations/types.js +25 -0
- package/dist/shortcuts/meeting/operations.js +19 -0
- package/dist/shortcuts/yimiaoban/continuation.js +70 -0
- package/dist/shortcuts/yimiaoban/errors.js +61 -0
- package/dist/shortcuts/yimiaoban/host.js +469 -0
- package/dist/shortcuts/yimiaoban/index.js +271 -0
- package/dist/shortcuts/yimiaoban/manifest.js +28 -0
- package/dist/shortcuts/yimiaoban/operations/file-user.js +288 -0
- package/dist/shortcuts/yimiaoban/operations/group-read.js +477 -0
- package/dist/shortcuts/yimiaoban/operations/group-write.js +591 -0
- package/dist/shortcuts/yimiaoban/operations/msg-parse.js +429 -0
- package/dist/shortcuts/yimiaoban/operations/msg-sync.js +519 -0
- package/dist/shortcuts/yimiaoban/operations/msg-write.js +593 -0
- package/dist/shortcuts/yimiaoban/operations/person.js +102 -0
- package/dist/shortcuts/yimiaoban/operations/registry.js +38 -0
- package/dist/shortcuts/yimiaoban/operations/session.js +437 -0
- package/dist/shortcuts/yimiaoban/operations/shared.js +236 -0
- package/dist/shortcuts/yimiaoban/operations/types.js +27 -0
- package/dist/shortcuts/yimiaoban/operations/write-util.js +78 -0
- package/dist/shortcuts/yimiaoban/operations.js +8 -0
- package/docs/_catalog.md +6 -0
- package/docs/meeting.md +48 -0
- package/docs/yimiaoban.md +86 -0
- package/package.json +1 -1
- package/skill-template/business-info.json +4 -1
- package/skill-template/domains/meeting.md +3 -0
- package/skill-template/domains/yimiaoban.md +21 -0
- package/skills/weaver-e10-calendar/SKILL.md +1 -0
- package/skills/weaver-e10-esb/SKILL.md +1 -0
- package/skills/weaver-e10-esb/references/source-manifest.json +3 -3
- package/skills/weaver-e10-hrm/SKILL.md +1 -0
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -0
- package/skills/weaver-e10-jucailin/SKILL.md +1 -0
- package/skills/weaver-e10-mail/SKILL.md +1 -0
- package/skills/weaver-e10-meeting/SKILL.md +106 -0
- package/skills/weaver-e10-meeting/product.json +8 -0
- package/skills/weaver-e10-meeting/references/meeting-change.md +67 -0
- package/skills/weaver-e10-meeting/references/meeting-create.md +52 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +57 -0
- package/skills/weaver-e10-meeting/references/meeting-queries.md +61 -0
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +48 -0
- package/skills/weaver-e10-meeting/references/meeting-update.md +7 -0
- package/skills/weaver-e10-meeting/references/source-manifest.json +50 -0
- package/skills/weaver-e10-plan/SKILL.md +1 -0
- package/skills/weaver-e10-plan/references/source-manifest.json +3 -3
- package/skills/weaver-e10-qiyecheng/SKILL.md +1 -0
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +3 -3
- package/skills/weaver-e10-shared/SKILL.md +4 -0
- package/skills/weaver-e10-skill-maker/SKILL.md +1 -0
- package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
- package/skills/weaver-e10-wenshuding/SKILL.md +1 -0
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +5 -5
- package/skills/weaver-e10-yimiaoban/SKILL.md +104 -0
- package/skills/weaver-e10-yimiaoban/product.json +8 -0
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +68 -0
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +85 -0
- package/skills/weaver-e10-yimiaoban/references/field-resolution.md +46 -0
- package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +54 -0
- package/skills/weaver-e10-yimiaoban/references/group-read.md +55 -0
- package/skills/weaver-e10-yimiaoban/references/group-write.md +72 -0
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +59 -0
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +64 -0
- package/skills/weaver-e10-yimiaoban/references/person.md +44 -0
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +61 -0
- package/skills/weaver-e10-yimiaoban/references/session-sysmsg.md +47 -0
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +485 -0
- package/skills/weaver-e10-ziguanjia/SKILL.md +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { ExitCode, isCliError, toErrorEnvelope } from '../../core/errors.js';
|
|
3
|
+
import { createYmbContinuationSigner } from './continuation.js';
|
|
4
|
+
import { ymbValidationError } from './errors.js';
|
|
5
|
+
import { createYmbHost } from './host.js';
|
|
6
|
+
import { ymbManifest } from './manifest.js';
|
|
7
|
+
import { executeYmbOperation } from './operations.js';
|
|
8
|
+
function asOptions(value) {
|
|
9
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
|
+
}
|
|
11
|
+
async function readStdin() {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for await (const chunk of process.stdin) {
|
|
14
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
15
|
+
}
|
|
16
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
17
|
+
}
|
|
18
|
+
function stripBom(value) {
|
|
19
|
+
return value.replace(/^\uFEFF+/, '');
|
|
20
|
+
}
|
|
21
|
+
export async function inputFrom(options, extra = {}) {
|
|
22
|
+
const opts = asOptions(options);
|
|
23
|
+
let input = {};
|
|
24
|
+
if (opts.input && opts.inputJson) {
|
|
25
|
+
throw ymbValidationError('input_conflict', '不能同时传 --input 和 --input-json');
|
|
26
|
+
}
|
|
27
|
+
if (opts.inputJson) {
|
|
28
|
+
input = parseJsonInput(String(opts.inputJson), '--input-json');
|
|
29
|
+
}
|
|
30
|
+
if (opts.input) {
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = stripBom(opts.input === '-' ? await readStdin() : await readFile(String(opts.input), 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw ymbValidationError('input_unreadable', `无法读取 --input:${String(opts.input)}`);
|
|
37
|
+
}
|
|
38
|
+
input = parseJsonInput(raw, '--input');
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...input,
|
|
42
|
+
...Object.fromEntries(Object.entries(extra).filter(([, value]) => value !== undefined && value !== '')),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function parseJsonInput(raw, label) {
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(stripBom(raw));
|
|
48
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
49
|
+
throw new Error('input must be an object');
|
|
50
|
+
}
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw ymbValidationError('input_invalid', `${label} 不是合法 JSON 对象`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function withJsonInputOptions(command) {
|
|
58
|
+
command
|
|
59
|
+
.option('--input <path>', 'JSON input file, or - for stdin')
|
|
60
|
+
.option('--input-json <json>', 'inline JSON input object');
|
|
61
|
+
return command;
|
|
62
|
+
}
|
|
63
|
+
function successEnvelope(operation, result) {
|
|
64
|
+
return {
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
ok: true,
|
|
67
|
+
operation,
|
|
68
|
+
// 业务数据为 undefined 时仍输出 data 键,避免调用方需额外判断键是否存在。
|
|
69
|
+
data: result.data === undefined ? null : result.data,
|
|
70
|
+
...(result.meta && Object.keys(result.meta).length ? { meta: result.meta } : {}),
|
|
71
|
+
warnings: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function failureEnvelope(error) {
|
|
75
|
+
const structured = error;
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
ok: false,
|
|
79
|
+
...(structured?.partialData === undefined ? {} : { data: structured.partialData }),
|
|
80
|
+
error: toErrorEnvelope(error),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function executeAndPrint(ctx, operation, input) {
|
|
84
|
+
try {
|
|
85
|
+
const result = await executeYmbOperation(operation, input, {
|
|
86
|
+
ctx,
|
|
87
|
+
host: createYmbHost(ctx),
|
|
88
|
+
continuation: createYmbContinuationSigner(ctx),
|
|
89
|
+
});
|
|
90
|
+
ctx.output.write(successEnvelope(operation, result), { json: true });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(JSON.stringify(failureEnvelope(error), null, 2));
|
|
94
|
+
process.exitCode = isCliError(error) ? error.exitCode : ExitCode.GeneralError;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function boolOption(value) {
|
|
98
|
+
if (value === undefined || value === null || value === '')
|
|
99
|
+
return undefined;
|
|
100
|
+
return value === true || value === 'true' || value === '1';
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 易秒办(即时通讯)CLI 命令注册。
|
|
104
|
+
* 所有操作统一走 `run <operation> --input-json '{}'`;便捷命令只覆盖高频只读场景,
|
|
105
|
+
* 写操作一律通过 `run ...prepare`(签发 continuation)→ `run ...apply --input-json '{"confirm":true,"continuation":"..."}'`
|
|
106
|
+
* 两段式执行,便捷层不绕过确认链。
|
|
107
|
+
*/
|
|
108
|
+
export const yimiaobanShortcut = {
|
|
109
|
+
name: 'yimiaoban',
|
|
110
|
+
description: '泛微E10易秒办(即时通讯)CLI',
|
|
111
|
+
category: 'shortcut',
|
|
112
|
+
register({ program, action }) {
|
|
113
|
+
const ymb = program.command('yimiaoban').description('泛微E10易秒办:消息/群组/会话/系统消息');
|
|
114
|
+
ymb.command('schema')
|
|
115
|
+
.description('Print yimiaoban operation manifest for agents')
|
|
116
|
+
.action(action((ctx) => {
|
|
117
|
+
ctx.output.write(ymbManifest, { json: true });
|
|
118
|
+
}));
|
|
119
|
+
withJsonInputOptions(ymb.command('run')
|
|
120
|
+
.description('Run a yimiaoban operation with JSON input')
|
|
121
|
+
.argument('<operation>', 'operation name, e.g. yimiaoban.msg.sync.group'))
|
|
122
|
+
.action(action(async (ctx, operation, options) => {
|
|
123
|
+
await executeAndPrint(ctx, String(operation), await inputFrom(options));
|
|
124
|
+
}));
|
|
125
|
+
// ---- 高频便捷命令:会话 ----
|
|
126
|
+
const session = ymb.command('session').description('Session commands');
|
|
127
|
+
withJsonInputOptions(session.command('list')
|
|
128
|
+
.description('拉取当前用户会话列表')
|
|
129
|
+
.option('--num <n>', '单页条数(默认 20,最多 50)')
|
|
130
|
+
.option('--msgid <id>', '锚点消息 id')
|
|
131
|
+
.option('--flag <0|1|2>', '0 up+down / 1 up / 2 down(默认 2)')
|
|
132
|
+
.option('--type <type>', '仅展示该 session_type'))
|
|
133
|
+
.action(action(async (ctx, options) => {
|
|
134
|
+
const opts = asOptions(options);
|
|
135
|
+
await executeAndPrint(ctx, 'yimiaoban.session.list', await inputFrom(options, {
|
|
136
|
+
num: opts.num !== undefined ? Number(opts.num) : undefined,
|
|
137
|
+
msgid: opts.msgid,
|
|
138
|
+
flag: opts.flag !== undefined ? Number(opts.flag) : undefined,
|
|
139
|
+
type: opts.type !== undefined ? Number(opts.type) : undefined,
|
|
140
|
+
}));
|
|
141
|
+
}));
|
|
142
|
+
// ---- 高频便捷命令:消息 ----
|
|
143
|
+
const msg = ymb.command('msg').description('Message commands');
|
|
144
|
+
withJsonInputOptions(msg.command('sync-single')
|
|
145
|
+
.description('拉取与某人的单聊消息')
|
|
146
|
+
.option('--from-uid <uid>', '对方 uid(必填)')
|
|
147
|
+
.option('--from-cid <cid>', '对方 cid(必填)')
|
|
148
|
+
.option('--num <n>', '单页数量(默认 20,最多 50)')
|
|
149
|
+
.option('--from <time>', '起始时间 YYYY-MM-DD[ HH:mm:ss] 或 unix 秒')
|
|
150
|
+
.option('--to <time>', '结束时间')
|
|
151
|
+
.option('--latest', '拉最新不限日期')
|
|
152
|
+
.option('--img-format <fmt>', 'small/large/original(默认 small)'))
|
|
153
|
+
.action(action(async (ctx, options) => {
|
|
154
|
+
const opts = asOptions(options);
|
|
155
|
+
await executeAndPrint(ctx, 'yimiaoban.msg.sync.single', await inputFrom(options, {
|
|
156
|
+
fromUid: opts.fromUid,
|
|
157
|
+
fromCid: opts.fromCid,
|
|
158
|
+
num: opts.num !== undefined ? Number(opts.num) : undefined,
|
|
159
|
+
from: opts.from,
|
|
160
|
+
to: opts.to,
|
|
161
|
+
latest: boolOption(opts.latest),
|
|
162
|
+
imgFormat: opts.imgFormat,
|
|
163
|
+
}));
|
|
164
|
+
}));
|
|
165
|
+
withJsonInputOptions(msg.command('sync-group')
|
|
166
|
+
.description('拉取某群聊天消息')
|
|
167
|
+
.option('--gid <id>', '群 id(必填)')
|
|
168
|
+
.option('--num <n>', '单页数量(默认 20,最多 50)')
|
|
169
|
+
.option('--from <time>', '起始时间')
|
|
170
|
+
.option('--to <time>', '结束时间')
|
|
171
|
+
.option('--latest', '拉最新不限日期')
|
|
172
|
+
.option('--start-id <id>', '显式起始消息 id')
|
|
173
|
+
.option('--end-id <id>', '显式结束消息 id')
|
|
174
|
+
.option('--img-format <fmt>', 'small/large/original(默认 small)'))
|
|
175
|
+
.action(action(async (ctx, options) => {
|
|
176
|
+
const opts = asOptions(options);
|
|
177
|
+
await executeAndPrint(ctx, 'yimiaoban.msg.sync.group', await inputFrom(options, {
|
|
178
|
+
groupId: opts.gid,
|
|
179
|
+
num: opts.num !== undefined ? Number(opts.num) : undefined,
|
|
180
|
+
from: opts.from,
|
|
181
|
+
to: opts.to,
|
|
182
|
+
latest: boolOption(opts.latest),
|
|
183
|
+
startId: opts.startId,
|
|
184
|
+
endId: opts.endId,
|
|
185
|
+
imgFormat: opts.imgFormat,
|
|
186
|
+
}));
|
|
187
|
+
}));
|
|
188
|
+
withJsonInputOptions(msg.command('top')
|
|
189
|
+
.description('同步群置顶消息')
|
|
190
|
+
.option('--gid <id>', '群 id(必填)'))
|
|
191
|
+
.action(action(async (ctx, options) => {
|
|
192
|
+
const opts = asOptions(options);
|
|
193
|
+
await executeAndPrint(ctx, 'yimiaoban.msg.top.sync', await inputFrom(options, { groupId: opts.gid }));
|
|
194
|
+
}));
|
|
195
|
+
// ---- 高频便捷命令:群组 ----
|
|
196
|
+
const group = ymb.command('group').description('Group commands');
|
|
197
|
+
withJsonInputOptions(group.command('search')
|
|
198
|
+
.description('按条件搜索群(服务端过滤)')
|
|
199
|
+
.option('--name <kw>', '群名关键字')
|
|
200
|
+
.option('--precise', '精确匹配名称')
|
|
201
|
+
.option('--type <type>', '群类型(1 普通/6 部门/7 全员等)')
|
|
202
|
+
.option('--members', '我所在的群')
|
|
203
|
+
.option('--owner', '我是群主的群')
|
|
204
|
+
.option('--creator', '我创建的群')
|
|
205
|
+
.option('--page-size <n>', '每页大小(默认 100,最多 200)'))
|
|
206
|
+
.action(action(async (ctx, options) => {
|
|
207
|
+
const opts = asOptions(options);
|
|
208
|
+
await executeAndPrint(ctx, 'yimiaoban.group.search', await inputFrom(options, {
|
|
209
|
+
name: opts.name,
|
|
210
|
+
precise: boolOption(opts.precise),
|
|
211
|
+
type: opts.type !== undefined ? Number(opts.type) : undefined,
|
|
212
|
+
members: boolOption(opts.members),
|
|
213
|
+
owner: boolOption(opts.owner),
|
|
214
|
+
creator: boolOption(opts.creator),
|
|
215
|
+
pageSize: opts.pageSize !== undefined ? Number(opts.pageSize) : undefined,
|
|
216
|
+
}));
|
|
217
|
+
}));
|
|
218
|
+
withJsonInputOptions(group.command('info')
|
|
219
|
+
.description('按群 id 查基础信息(支持批量,逗号分隔)')
|
|
220
|
+
.option('--gid <ids>', '群 id(逗号分隔或单值)'))
|
|
221
|
+
.action(action(async (ctx, options) => {
|
|
222
|
+
const opts = asOptions(options);
|
|
223
|
+
await executeAndPrint(ctx, 'yimiaoban.group.info', await inputFrom(options, { groupIds: opts.gid }));
|
|
224
|
+
}));
|
|
225
|
+
withJsonInputOptions(group.command('users')
|
|
226
|
+
.description('拉取群成员列表')
|
|
227
|
+
.option('--gid <id>', '群 id(必填)'))
|
|
228
|
+
.action(action(async (ctx, options) => {
|
|
229
|
+
const opts = asOptions(options);
|
|
230
|
+
await executeAndPrint(ctx, 'yimiaoban.group.users', await inputFrom(options, { groupId: opts.gid }));
|
|
231
|
+
}));
|
|
232
|
+
// ---- 高频便捷命令:人员解析 ----
|
|
233
|
+
withJsonInputOptions(ymb.command('resolve')
|
|
234
|
+
.description('按姓名/工号/手机/邮箱模糊查询在职员工')
|
|
235
|
+
.option('--name <kw>', '查询关键字(必填)'))
|
|
236
|
+
.action(action(async (ctx, options) => {
|
|
237
|
+
const opts = asOptions(options);
|
|
238
|
+
await executeAndPrint(ctx, 'yimiaoban.person.resolve', await inputFrom(options, { name: opts.name }));
|
|
239
|
+
}));
|
|
240
|
+
// ---- 高频便捷命令:发消息(两段式,仅演示 prepare 链) ----
|
|
241
|
+
const send = ymb.command('send').description('Send message commands (two-phase prepare -> apply)');
|
|
242
|
+
withJsonInputOptions(send.command('single-prepare')
|
|
243
|
+
.description('准备发送单聊消息(只读校验并签发 continuation)')
|
|
244
|
+
.option('--to-uid <uid>', '接收人 uid(必填)')
|
|
245
|
+
.option('--to-cid <cid>', '接收人 cid(必填)')
|
|
246
|
+
.option('--txt <text>', '文本内容')
|
|
247
|
+
.option('--img <path>', '本地图片路径')
|
|
248
|
+
.option('--file <path>', '本地文件路径'))
|
|
249
|
+
.action(action(async (ctx, options) => {
|
|
250
|
+
const opts = asOptions(options);
|
|
251
|
+
await executeAndPrint(ctx, 'yimiaoban.msg.send.single.prepare', await inputFrom(options, {
|
|
252
|
+
toUid: opts.toUid,
|
|
253
|
+
toCid: opts.toCid,
|
|
254
|
+
txt: opts.txt,
|
|
255
|
+
img: opts.img,
|
|
256
|
+
file: opts.file,
|
|
257
|
+
}));
|
|
258
|
+
}));
|
|
259
|
+
withJsonInputOptions(send.command('single-apply')
|
|
260
|
+
.description('执行发送单聊消息(需 --yes 与 continuation)')
|
|
261
|
+
.option('--continuation <token>', 'continuation token from send single-prepare')
|
|
262
|
+
.option('--yes', 'confirm the write operation'))
|
|
263
|
+
.action(action(async (ctx, options) => {
|
|
264
|
+
const opts = asOptions(options);
|
|
265
|
+
await executeAndPrint(ctx, 'yimiaoban.msg.send.single.apply', await inputFrom(options, {
|
|
266
|
+
continuation: opts.continuation,
|
|
267
|
+
confirm: opts.yes === true ? true : undefined,
|
|
268
|
+
}));
|
|
269
|
+
}));
|
|
270
|
+
},
|
|
271
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ymbOperationDefinitions } from './operations/registry.js';
|
|
2
|
+
function buildOperations() {
|
|
3
|
+
return ymbOperationDefinitions().map((definition) => ({
|
|
4
|
+
name: definition.name,
|
|
5
|
+
resource: definition.resource,
|
|
6
|
+
summary: definition.summary,
|
|
7
|
+
...(definition.capability ? { capability: definition.capability } : {}),
|
|
8
|
+
risk: definition.risk,
|
|
9
|
+
...(definition.requiresConfirmation ? { requiresConfirmation: true } : {}),
|
|
10
|
+
inputSchema: definition.inputSchema,
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 易秒办(即时通讯)模块 manifest。
|
|
15
|
+
* operations 清单直接来自 registry(含全部 prepare/apply 成对写操作),
|
|
16
|
+
* 供 `weaver-work-cli yimiaoban schema` 输出给 Agent/Skill 使用。
|
|
17
|
+
*/
|
|
18
|
+
export const ymbManifest = {
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
name: 'yimiaoban',
|
|
21
|
+
title: '泛微E10易秒办(即时通讯:消息/群组/会话/系统消息)',
|
|
22
|
+
version: '0.1.0',
|
|
23
|
+
host: 'e10-yimiaoban',
|
|
24
|
+
status: 'builtin',
|
|
25
|
+
sourceSkill: 'weaver-e10-yimiaoban',
|
|
26
|
+
capabilities: ['person', 'msg', 'ding', 'group', 'session', 'sysmsg', 'file', 'user', 'i18n'],
|
|
27
|
+
operations: buildOperations(),
|
|
28
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
4
|
+
import { ymbValidationError } from '../errors.js';
|
|
5
|
+
import { READ, schemaOf, ymbSuccess, } from './types.js';
|
|
6
|
+
import { assertImOk, compact, splitIds, str, toUint, unwrapInner } from './shared.js';
|
|
7
|
+
import { fileObjToFile, fileObjToImg } from './write-util.js';
|
|
8
|
+
/** 文件上传(两段式,module=im),返回 fileObj + 消息对象两种形态。 */
|
|
9
|
+
const fileUpload = {
|
|
10
|
+
name: 'yimiaoban.file.upload',
|
|
11
|
+
resource: 'file',
|
|
12
|
+
method: 'file.upload',
|
|
13
|
+
action: 'yimiaoban.file.upload',
|
|
14
|
+
summary: '上传本地文件到 E10 文件服务(module=im),返回文件 id(供消息 img/file 引用)',
|
|
15
|
+
capability: 'file',
|
|
16
|
+
risk: 'external-artifact',
|
|
17
|
+
inputSchema: schemaOf({
|
|
18
|
+
file: { type: 'string', description: '本地文件路径(必填,兼容旧名 filePath)' },
|
|
19
|
+
name: { type: 'string', description: '文件名(默认取 basename)' },
|
|
20
|
+
shareGroup: { type: 'string', description: '分享群 id(发群消息用;兼容 shareGroups 逗号串)' },
|
|
21
|
+
shareUsers: { type: 'array', items: { type: 'string' }, description: '分享用户 uid(发单聊用)' },
|
|
22
|
+
permission: { type: 'string', description: '文件权限(默认 3)' },
|
|
23
|
+
}, { required: ['file'], description: '文件上传' }),
|
|
24
|
+
handler: async (args, { host }) => {
|
|
25
|
+
const filePath = String(args.file ?? args.filePath ?? '');
|
|
26
|
+
if (!filePath)
|
|
27
|
+
throw ymbValidationError('file_required', 'file 必填');
|
|
28
|
+
const abs = resolve(filePath);
|
|
29
|
+
if (!existsSync(abs))
|
|
30
|
+
throw ymbValidationError('file_not_found', `文件不存在: ${abs}`);
|
|
31
|
+
const shareUsers = Array.isArray(args.shareUsers) ? args.shareUsers.join(',') : String(args.shareUsers ?? '');
|
|
32
|
+
const shareGroups = args.shareGroup ?? args.shareGroups;
|
|
33
|
+
const fileObj = await host.uploadFile(abs, {
|
|
34
|
+
name: args.name !== undefined ? String(args.name) : undefined,
|
|
35
|
+
shareGroups: shareGroups !== undefined && shareGroups !== '' ? String(shareGroups) : undefined,
|
|
36
|
+
shareUsers: shareUsers || undefined,
|
|
37
|
+
permission: args.permission !== undefined ? String(args.permission) : undefined,
|
|
38
|
+
});
|
|
39
|
+
return ymbSuccess('yimiaoban.file.upload', {
|
|
40
|
+
fileObj,
|
|
41
|
+
img: fileObjToImg(fileObj),
|
|
42
|
+
file: fileObjToFile(fileObj),
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/** 媒体预览(img/video)URL 构造 + 可选下载。 */
|
|
47
|
+
const filePreview = {
|
|
48
|
+
name: 'yimiaoban.file.preview',
|
|
49
|
+
resource: 'file',
|
|
50
|
+
method: 'file.preview',
|
|
51
|
+
action: 'yimiaoban.file.preview',
|
|
52
|
+
summary: '生成图片/视频预览与下载 URL(需登录态访问;图片支持 small/large/original)',
|
|
53
|
+
capability: 'file',
|
|
54
|
+
risk: READ,
|
|
55
|
+
inputSchema: schemaOf({
|
|
56
|
+
kind: { type: 'string', enum: ['img', 'video'], description: 'img(默认)/ video' },
|
|
57
|
+
fileId: { type: 'string', minLength: 1, description: '文件 id' },
|
|
58
|
+
msgid: { type: 'string', pattern: '^\\d+$', description: '消息 id' },
|
|
59
|
+
groupId: { type: 'string', description: '群聊场景群 id' },
|
|
60
|
+
toUid: { type: 'string', description: '单聊场景对方 uid' },
|
|
61
|
+
toCid: { type: 'string', description: '单聊场景对方 cid' },
|
|
62
|
+
imgFormat: { type: 'string', enum: ['small', 'large', 'original'], description: '图片规格(默认 small)' },
|
|
63
|
+
}, { required: ['fileId', 'msgid'], description: '媒体预览 URL' }),
|
|
64
|
+
handler: async (args, { host }) => {
|
|
65
|
+
const kind = String(args.kind ?? 'img') === 'video' ? 'video' : 'img';
|
|
66
|
+
const fileId = String(args.fileId);
|
|
67
|
+
const msgid = String(args.msgid);
|
|
68
|
+
const groupId = args.groupId !== undefined && args.groupId !== '' ? String(args.groupId) : undefined;
|
|
69
|
+
const user = args.toUid || args.toCid ? { cid: str(args.toCid), uid: str(args.toUid) } : undefined;
|
|
70
|
+
if (!groupId && !user) {
|
|
71
|
+
throw ymbValidationError('context_required', 'groupId(群聊)或 toUid+toCid(单聊)必须提供其一');
|
|
72
|
+
}
|
|
73
|
+
const opts = {
|
|
74
|
+
fileId,
|
|
75
|
+
msgid,
|
|
76
|
+
kind: kind,
|
|
77
|
+
imgFormat: (args.imgFormat || 'small'),
|
|
78
|
+
...(groupId ? { groupId } : {}),
|
|
79
|
+
...(user ? { user } : {}),
|
|
80
|
+
};
|
|
81
|
+
return ymbSuccess('yimiaoban.file.preview', {
|
|
82
|
+
kind,
|
|
83
|
+
fileId,
|
|
84
|
+
previewUrl: await host.buildPreviewUrl(opts),
|
|
85
|
+
downloadUrl: await host.buildDownloadUrl(opts),
|
|
86
|
+
note: '预览/下载 URL 需要登录态(Cookie+eteamsid),直接嵌入 Markdown 会 401;需要真正可见请用 yimiaoban.file.download 落地到本地。',
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
/** 图片文件头签名(用于纠正扩展名:预览小图服务端会统一转 PNG)。 */
|
|
91
|
+
const IMAGE_SIGNATURES = [
|
|
92
|
+
{ ext: '.png', test: (b) => b.length >= 4 && b.subarray(0, 4).toString('hex') === '89504e47' },
|
|
93
|
+
{ ext: '.jpg', test: (b) => b.length >= 3 && b.subarray(0, 3).toString('hex') === 'ffd8ff' },
|
|
94
|
+
{ ext: '.gif', test: (b) => b.length >= 4 && b.subarray(0, 4).toString('hex') === '47494638' },
|
|
95
|
+
{
|
|
96
|
+
ext: '.webp',
|
|
97
|
+
test: (b) => b.length >= 12
|
|
98
|
+
&& b.subarray(0, 4).toString('ascii') === 'RIFF'
|
|
99
|
+
&& b.subarray(8, 12).toString('ascii') === 'WEBP',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
/** 按文件头识别图片真实格式;不是已知图片格式时返回 null。 */
|
|
103
|
+
function detectImageExt(buffer) {
|
|
104
|
+
for (const signature of IMAGE_SIGNATURES) {
|
|
105
|
+
if (signature.test(buffer))
|
|
106
|
+
return signature.ext;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/** 消息媒体/文件落地到本地(需登录态;父目录必须存在,禁止覆盖已存在文件)。 */
|
|
111
|
+
const fileDownload = {
|
|
112
|
+
name: 'yimiaoban.file.download',
|
|
113
|
+
resource: 'file',
|
|
114
|
+
method: 'file.download',
|
|
115
|
+
action: 'yimiaoban.file.download',
|
|
116
|
+
summary: '下载消息里的图片/视频/文件到本地(需登录态;父目录必须存在,禁止覆盖)',
|
|
117
|
+
capability: 'file',
|
|
118
|
+
risk: 'local-file-write',
|
|
119
|
+
inputSchema: schemaOf({
|
|
120
|
+
fileId: { type: 'string', minLength: 1, description: '文件 id(取消息 media[].fileId)' },
|
|
121
|
+
msgid: { type: 'string', pattern: '^\\d+$', description: '消息 id(取消息 media[].msgid / msgid)' },
|
|
122
|
+
kind: { type: 'string', enum: ['img', 'video', 'file'], description: '媒体类型(默认 img;文件消息传 file)' },
|
|
123
|
+
groupId: { type: 'string', description: '群聊场景群 id(与单聊身份二选一)' },
|
|
124
|
+
toUid: { type: 'string', description: '单聊场景对方 uid(与 groupId 二选一)' },
|
|
125
|
+
toCid: { type: 'string', description: '单聊场景对方 cid(与 groupId 二选一)' },
|
|
126
|
+
imgFormat: { type: 'string', enum: ['small', 'large', 'original'], description: '图片规格(默认 small;要原图传 original)' },
|
|
127
|
+
output: { type: 'string', minLength: 1, description: '本地输出文件路径(父目录必须存在,已存在则拒绝)' },
|
|
128
|
+
}, { required: ['fileId', 'msgid', 'output'], description: '媒体/文件落地到本地' }),
|
|
129
|
+
handler: async (args, { host }) => {
|
|
130
|
+
const fileId = String(args.fileId ?? '');
|
|
131
|
+
const msgid = String(args.msgid ?? '');
|
|
132
|
+
const kindRaw = String(args.kind ?? 'img');
|
|
133
|
+
const kind = kindRaw === 'video' ? 'video' : 'img';
|
|
134
|
+
const output = String(args.output ?? '').trim();
|
|
135
|
+
if (!output)
|
|
136
|
+
throw ymbValidationError('output_required', 'output 必须是明确的本地文件路径');
|
|
137
|
+
const groupId = args.groupId !== undefined && args.groupId !== '' ? String(args.groupId) : undefined;
|
|
138
|
+
const user = args.toUid || args.toCid ? { cid: str(args.toCid), uid: str(args.toUid) } : undefined;
|
|
139
|
+
if (!groupId && !user) {
|
|
140
|
+
throw ymbValidationError('context_required', 'groupId(群聊)或 toUid+toCid(单聊)必须提供其一');
|
|
141
|
+
}
|
|
142
|
+
const resolved = resolve(output);
|
|
143
|
+
const parent = await stat(dirname(resolved)).catch(() => null);
|
|
144
|
+
if (!parent)
|
|
145
|
+
throw ymbValidationError('output_parent_missing', `输出目录不存在:${dirname(resolved)}`);
|
|
146
|
+
if (!parent.isDirectory())
|
|
147
|
+
throw ymbValidationError('output_parent_invalid', '输出文件的父路径不是目录');
|
|
148
|
+
if (existsSync(resolved))
|
|
149
|
+
throw ymbValidationError('output_exists', `输出文件已存在,拒绝覆盖:${resolved}`);
|
|
150
|
+
const url = await host.buildDownloadUrl({
|
|
151
|
+
fileId,
|
|
152
|
+
msgid,
|
|
153
|
+
kind: kind,
|
|
154
|
+
imgFormat: (args.imgFormat || 'small'),
|
|
155
|
+
...(groupId ? { groupId } : {}),
|
|
156
|
+
...(user ? { user } : {}),
|
|
157
|
+
});
|
|
158
|
+
const content = await host.requestBinary(url);
|
|
159
|
+
await writeFile(resolved, content, { flag: 'wx', mode: 0o600 });
|
|
160
|
+
// 图片(含以 file 形态下载的图片):按文件头纠正扩展名,避免「.jpg 里其实是 PNG」
|
|
161
|
+
let finalPath = resolved;
|
|
162
|
+
let renamedFrom;
|
|
163
|
+
if (kind === 'img' || kindRaw === 'file') {
|
|
164
|
+
const detected = detectImageExt(content);
|
|
165
|
+
const currentExt = extname(resolved).toLowerCase();
|
|
166
|
+
if (detected && detected !== currentExt) {
|
|
167
|
+
const candidate = currentExt
|
|
168
|
+
? `${resolved.slice(0, resolved.length - currentExt.length)}${detected}`
|
|
169
|
+
: `${resolved}${detected}`;
|
|
170
|
+
if (!existsSync(candidate)) {
|
|
171
|
+
await rename(resolved, candidate);
|
|
172
|
+
renamedFrom = resolved;
|
|
173
|
+
finalPath = candidate;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return ymbSuccess('yimiaoban.file.download', {
|
|
178
|
+
fileId,
|
|
179
|
+
msgid,
|
|
180
|
+
kind: kindRaw,
|
|
181
|
+
output: finalPath,
|
|
182
|
+
size: content.length,
|
|
183
|
+
...(renamedFrom
|
|
184
|
+
? { renamedFrom, renameNote: '扩展名已按文件头纠正(small/large 预览为服务端转换后的格式)' }
|
|
185
|
+
: {}),
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
/** 人员在线状态。 */
|
|
190
|
+
const userState = {
|
|
191
|
+
name: 'yimiaoban.user.state',
|
|
192
|
+
resource: 'user',
|
|
193
|
+
method: 'user.state',
|
|
194
|
+
action: 'yimiaoban.user.state',
|
|
195
|
+
summary: '查询指定人员各在线设备状态(sub_state 强制 0 不订阅)',
|
|
196
|
+
capability: 'user',
|
|
197
|
+
risk: READ,
|
|
198
|
+
inputSchema: schemaOf({
|
|
199
|
+
users: { type: 'array', items: { type: 'string' }, description: '人员 uid 列表' },
|
|
200
|
+
devType: { type: 'integer', description: '设备类型(缺省查全部在线设备)' },
|
|
201
|
+
mask: { type: 'integer', description: '信息掩码(默认 7)' },
|
|
202
|
+
}, { required: ['users'], description: '查询人员在线状态' }),
|
|
203
|
+
handler: async (args, { host }) => {
|
|
204
|
+
const rawUids = Array.isArray(args.users) ? args.users.map((x) => String(x)) : splitIds(args.users, 'users');
|
|
205
|
+
const map = await host.resolveUsersByIds(rawUids);
|
|
206
|
+
const members = [];
|
|
207
|
+
const failed = [];
|
|
208
|
+
for (const uid of rawUids) {
|
|
209
|
+
const p = map[uid];
|
|
210
|
+
if (!p?.cid || p.cid === '0') {
|
|
211
|
+
failed.push({ uid, name: p?.name || '' });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
members.push({ cid: p.cid, uid });
|
|
215
|
+
}
|
|
216
|
+
if (failed.length && args.removeFailed !== true) {
|
|
217
|
+
throw ymbValidationError('members_resolve_failed', `以下人员解析失败:${failed.map((f) => f.name || f.uid).join('、')};确认跳过请传 removeFailed:true`);
|
|
218
|
+
}
|
|
219
|
+
const body = compact({
|
|
220
|
+
users: members,
|
|
221
|
+
sub_state: 0,
|
|
222
|
+
dev_type: args.devType !== undefined ? toUint(args.devType, 'devType') : undefined,
|
|
223
|
+
mask: Number(args.mask ?? 7),
|
|
224
|
+
});
|
|
225
|
+
const res = await host.request({
|
|
226
|
+
method: 'POST',
|
|
227
|
+
path: '/api/em/msg/executeIm/state/syncUserState',
|
|
228
|
+
body,
|
|
229
|
+
operation: 'yimiaoban.user.state',
|
|
230
|
+
});
|
|
231
|
+
assertImOk(res, '查询人员状态');
|
|
232
|
+
const inner = unwrapInner(res);
|
|
233
|
+
const devTypeNames = {
|
|
234
|
+
1: 'Android', 2: 'iPhone', 3: 'iPad', 4: 'API', 5: 'PC Web', 6: '手机Web', 7: 'Mac', 8: 'Linux', 9: 'Windows', 10: '第三方', 11: '安卓Pad', 12: '鸿蒙', 13: '鸿蒙Pad', 0: '无效',
|
|
235
|
+
};
|
|
236
|
+
const infos = (inner?.infos || []).map((it) => {
|
|
237
|
+
const uid = str(it?.user?.uid);
|
|
238
|
+
const devices = (it?.dev_infos || []).map((d) => ({
|
|
239
|
+
dev: Number(d.dev ?? -1),
|
|
240
|
+
devName: devTypeNames[Number(d.dev)] ?? String(d.dev),
|
|
241
|
+
status: Number(d.status),
|
|
242
|
+
online: Number(d.status) === 1,
|
|
243
|
+
loginTime: formatTime(d.logintime),
|
|
244
|
+
activeTime: formatTime(d.active_time),
|
|
245
|
+
imei: str(d.imei),
|
|
246
|
+
}));
|
|
247
|
+
return { uid, name: map[uid]?.name || uid, devices };
|
|
248
|
+
});
|
|
249
|
+
return ymbSuccess('yimiaoban.user.state', { total: infos.length, infos });
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
function formatTime(ts) {
|
|
253
|
+
if (ts === undefined || ts === null || ts === '')
|
|
254
|
+
return '-';
|
|
255
|
+
const d = new Date(Number(ts) * 1000);
|
|
256
|
+
if (Number.isNaN(d.getTime()))
|
|
257
|
+
return '-';
|
|
258
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
259
|
+
return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
260
|
+
}
|
|
261
|
+
/** i18n 标签翻译(内部支撑,也可单独查询)。 */
|
|
262
|
+
const i18nLabels = {
|
|
263
|
+
name: 'yimiaoban.i18n.labels',
|
|
264
|
+
resource: 'i18n',
|
|
265
|
+
method: 'i18n.labels',
|
|
266
|
+
action: 'yimiaoban.i18n.labels',
|
|
267
|
+
summary: '批量翻译国际化标签 id → 当前语言文案',
|
|
268
|
+
capability: 'i18n',
|
|
269
|
+
risk: READ,
|
|
270
|
+
inputSchema: schemaOf({
|
|
271
|
+
ids: { type: 'array', items: { type: 'string' }, description: '标签 id(数字)列表' },
|
|
272
|
+
}, { required: ['ids'], description: 'i18n 标签翻译' }),
|
|
273
|
+
handler: async (args, { host }) => {
|
|
274
|
+
const ids = Array.isArray(args.ids) ? args.ids.map((x) => String(x)) : splitIds(args.ids, 'ids');
|
|
275
|
+
const labels = await host.fetchI18nLabels(ids);
|
|
276
|
+
return ymbSuccess('yimiaoban.i18n.labels', {
|
|
277
|
+
total: Object.keys(labels).length,
|
|
278
|
+
labels: ids.map((id) => ({ id, text: labels[id] ?? '(未取到文案)' })),
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
export const fileUserOperations = [
|
|
283
|
+
fileUpload,
|
|
284
|
+
filePreview,
|
|
285
|
+
fileDownload,
|
|
286
|
+
userState,
|
|
287
|
+
i18nLabels,
|
|
288
|
+
];
|