team-anya 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/apps/server/dist/broker/cc-broker.js +267 -0
- package/apps/server/dist/cli.js +296 -0
- package/apps/server/dist/config.js +78 -0
- package/apps/server/dist/daemon.js +51 -0
- package/apps/server/dist/franky/context-builder.js +161 -0
- package/apps/server/dist/franky/franky-mcp-server.js +110 -0
- package/apps/server/dist/franky/franky-orchestrator.js +629 -0
- package/apps/server/dist/franky/index.js +5 -0
- package/apps/server/dist/franky/topic-router.js +16 -0
- package/apps/server/dist/gateway/chat-sync.js +135 -0
- package/apps/server/dist/gateway/command-router.js +116 -0
- package/apps/server/dist/gateway/commands/cancel.js +32 -0
- package/apps/server/dist/gateway/commands/help.js +16 -0
- package/apps/server/dist/gateway/commands/index.js +26 -0
- package/apps/server/dist/gateway/commands/restart.js +43 -0
- package/apps/server/dist/gateway/commands/status.js +34 -0
- package/apps/server/dist/gateway/commands/tasks.js +33 -0
- package/apps/server/dist/gateway/feishu-sender.js +508 -0
- package/apps/server/dist/gateway/feishu-ws.js +353 -0
- package/apps/server/dist/gateway/health-monitor.js +154 -0
- package/apps/server/dist/gateway/http.js +1064 -0
- package/apps/server/dist/gateway/media-downloader.js +182 -0
- package/apps/server/dist/gateway/message-events.js +10 -0
- package/apps/server/dist/gateway/message-intake.js +72 -0
- package/apps/server/dist/gateway/message-queue.js +118 -0
- package/apps/server/dist/gateway/session-reader.js +142 -0
- package/apps/server/dist/gateway/ws-push.js +115 -0
- package/apps/server/dist/loid/brain.js +121 -0
- package/apps/server/dist/loid/clarifier.js +162 -0
- package/apps/server/dist/loid/context-builder.js +462 -0
- package/apps/server/dist/loid/mcp-server.js +119 -0
- package/apps/server/dist/loid/memory-settler.js +189 -0
- package/apps/server/dist/loid/opportunity-manager.js +148 -0
- package/apps/server/dist/loid/profile-updater.js +179 -0
- package/apps/server/dist/loid/project-registry.js +192 -0
- package/apps/server/dist/loid/reporter.js +148 -0
- package/apps/server/dist/loid/schemas.js +117 -0
- package/apps/server/dist/loid/self-calibrator.js +314 -0
- package/apps/server/dist/loid/session-manager.js +472 -0
- package/apps/server/dist/loid/session.js +276 -0
- package/apps/server/dist/main.js +528 -0
- package/apps/server/dist/tracing/index.js +2 -0
- package/apps/server/dist/tracing/trace-context.js +92 -0
- package/apps/server/dist/types/message.js +2 -0
- package/apps/server/dist/yor/yor-mcp-server.js +107 -0
- package/apps/server/dist/yor/yor-orchestrator.js +248 -0
- package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
- package/apps/web/dist/assets/index-Dnb9LGZd.js +798 -0
- package/apps/web/dist/index.html +13 -0
- package/package.json +42 -0
- package/packages/cc-client/dist/claude-code-backend.js +792 -0
- package/packages/cc-client/dist/index.js +2 -0
- package/packages/cc-client/package.json +11 -0
- package/packages/core/dist/constants.js +60 -0
- package/packages/core/dist/errors.js +35 -0
- package/packages/core/dist/index.js +9 -0
- package/packages/core/dist/office-init.js +190 -0
- package/packages/core/dist/repo-cache.js +70 -0
- package/packages/core/dist/scope/checker.js +114 -0
- package/packages/core/dist/scope/defaults.js +55 -0
- package/packages/core/dist/scope/index.js +3 -0
- package/packages/core/dist/state-machine.js +86 -0
- package/packages/core/dist/types/audit.js +12 -0
- package/packages/core/dist/types/backend.js +2 -0
- package/packages/core/dist/types/commitment.js +17 -0
- package/packages/core/dist/types/communication.js +18 -0
- package/packages/core/dist/types/index.js +9 -0
- package/packages/core/dist/types/opportunity.js +27 -0
- package/packages/core/dist/types/org.js +26 -0
- package/packages/core/dist/types/task.js +46 -0
- package/packages/core/dist/types/workspace.js +39 -0
- package/packages/core/dist/workspace-manager.js +314 -0
- package/packages/core/package.json +10 -0
- package/packages/db/dist/client.js +69 -0
- package/packages/db/dist/index.js +756 -0
- package/packages/db/dist/schema/audit-events.js +13 -0
- package/packages/db/dist/schema/cc-sessions.js +14 -0
- package/packages/db/dist/schema/chats.js +35 -0
- package/packages/db/dist/schema/commitments.js +18 -0
- package/packages/db/dist/schema/communication-events.js +14 -0
- package/packages/db/dist/schema/index.js +14 -0
- package/packages/db/dist/schema/message-log.js +20 -0
- package/packages/db/dist/schema/opportunities.js +23 -0
- package/packages/db/dist/schema/org.js +36 -0
- package/packages/db/dist/schema/projects.js +23 -0
- package/packages/db/dist/schema/tasks.js +51 -0
- package/packages/db/dist/schema/topics.js +22 -0
- package/packages/db/dist/schema/trace-spans.js +19 -0
- package/packages/db/dist/schema/workspaces.js +15 -0
- package/packages/db/package.json +12 -0
- package/packages/db/src/migrations/0000_baseline.sql +251 -0
- package/packages/db/src/migrations/0001_workspaces.sql +19 -0
- package/packages/db/src/migrations/0002_workspace_parent.sql +1 -0
- package/packages/db/src/migrations/0003_chat_context.sql +3 -0
- package/packages/db/src/migrations/meta/_journal.json +34 -0
- package/packages/mcp-tools/dist/index.js +41 -0
- package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
- package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
- package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
- package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
- package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
- package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
- package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
- package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
- package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
- package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
- package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
- package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
- package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
- package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
- package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
- package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
- package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
- package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
- package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
- package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
- package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
- package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
- package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
- package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +206 -0
- package/packages/mcp-tools/dist/layer2/loid/task-escalate-to-topic.js +170 -0
- package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +45 -0
- package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
- package/packages/mcp-tools/dist/layer2/loid/topic-create.js +60 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +28 -0
- package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
- package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
- package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
- package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
- package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +203 -0
- package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
- package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
- package/packages/mcp-tools/dist/layer3/channel-send.js +75 -0
- package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
- package/packages/mcp-tools/dist/registry.js +911 -0
- package/packages/mcp-tools/package.json +13 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function yorRework(deps, input) {
|
|
2
|
+
const { yorOrchestrator, logger } = deps;
|
|
3
|
+
logger?.info(`[anya:pipeline] [Loid] yor.rework task=${input.task_id}`);
|
|
4
|
+
yorOrchestrator.sendRework(input.task_id, input.feedback);
|
|
5
|
+
return { task_id: input.task_id, sent: true };
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=yor-rework.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
export async function yorSpawn(deps, input) {
|
|
4
|
+
const { yorOrchestrator, logger } = deps;
|
|
5
|
+
// 校验 working_dir 是工作区根目录(应含角色文件),防止 Loid 误传 repo 子目录
|
|
6
|
+
let workingDir = input.working_dir;
|
|
7
|
+
if (!existsSync(join(workingDir, 'CLAUDE.md')) && !existsSync(join(workingDir, 'PROFILE.md'))) {
|
|
8
|
+
// 检查是否是 WS-xxx 目录名模式(合法工作区)—— 可能还没有角色文件(旧路径)
|
|
9
|
+
const dirName = basename(workingDir);
|
|
10
|
+
if (!dirName.startsWith('WS-') && !dirName.startsWith('ANYA-')) {
|
|
11
|
+
// 很可能误传了 repo 子目录,尝试用 brief_path 推断根目录
|
|
12
|
+
logger?.error(`[anya:pipeline] [Loid] yor.spawn 警告: working_dir "${workingDir}" 看起来不是工作区根目录(缺少 CLAUDE.md/PROFILE.md),` +
|
|
13
|
+
`请确保传入 task.dispatch 返回的原始 working_dir`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
logger?.info(`[anya:pipeline] [Loid] yor.spawn ${input.task_id} | dir=${workingDir}`);
|
|
17
|
+
const result = await yorOrchestrator.spawnAndExecute({
|
|
18
|
+
taskId: input.task_id,
|
|
19
|
+
workingDir,
|
|
20
|
+
briefPath: input.brief_path,
|
|
21
|
+
env: input.env,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
instance_id: result.instanceId,
|
|
25
|
+
task_id: input.task_id,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=yor-spawn.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 报告阻塞
|
|
3
|
+
*
|
|
4
|
+
* Yor 遇到无法解决的问题时调用。
|
|
5
|
+
* 增加 category 字段与 PROTOCOL.md 对齐。
|
|
6
|
+
*/
|
|
7
|
+
export async function taskBlock(collector, input) {
|
|
8
|
+
collector.onBlockerReport(input);
|
|
9
|
+
return { received: true, type: 'blocker' };
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=task-block.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFile = promisify(execFileCb);
|
|
4
|
+
/**
|
|
5
|
+
* 提交交付成果
|
|
6
|
+
*
|
|
7
|
+
* Yor 完成任务后调用,报告变更文件、测试结果、PR 链接。
|
|
8
|
+
* 结果通过 collector 回调传给 dispatcher。
|
|
9
|
+
*
|
|
10
|
+
* 交付前检查 git 状态:如果有未提交变更,拒绝交付。
|
|
11
|
+
*/
|
|
12
|
+
export async function taskDeliver(collector, input, workingDir) {
|
|
13
|
+
// 检查 git 状态
|
|
14
|
+
if (workingDir) {
|
|
15
|
+
try {
|
|
16
|
+
// 检查是否是 git 仓库
|
|
17
|
+
await execFile('git', ['-C', workingDir, 'rev-parse', '--git-dir']);
|
|
18
|
+
// 检查是否有未提交变更
|
|
19
|
+
const { stdout } = await execFile('git', ['-C', workingDir, 'status', '--porcelain']);
|
|
20
|
+
if (stdout.trim().length > 0) {
|
|
21
|
+
return {
|
|
22
|
+
received: false,
|
|
23
|
+
type: 'delivery_rejected',
|
|
24
|
+
reason: `❌ 交付被拒绝:工作目录有未提交的变更。\n请先执行 git add + git commit,然后重新调用 task.deliver。\n未提交文件:\n${stdout.trim()}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// 不是 git 仓库或 git 命令失败,跳过检查(warn 级别)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
collector.onDeliverySubmit(input);
|
|
33
|
+
return { received: true, type: 'delivery' };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=task-deliver.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
/**
|
|
3
|
+
* 中间进度汇报
|
|
4
|
+
*
|
|
5
|
+
* 长任务中 Yor 向 Loid 汇报里程碑,不改变任务状态。
|
|
6
|
+
* 写 audit event(yor_progress),Loid 可通过 audit.query 查看。
|
|
7
|
+
*/
|
|
8
|
+
export async function taskProgress(db, taskId, input) {
|
|
9
|
+
insertAuditEvent(db, {
|
|
10
|
+
event_type: 'yor_progress',
|
|
11
|
+
actor: 'yor',
|
|
12
|
+
task_id: taskId,
|
|
13
|
+
summary: input.milestone,
|
|
14
|
+
detail: JSON.stringify({
|
|
15
|
+
detail: input.detail,
|
|
16
|
+
next_step: input.next_step,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
return { received: true };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=task-progress.js.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// ── 媒体类型检测 ──
|
|
2
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.ico', '.tiff', '.tif']);
|
|
3
|
+
const AUDIO_EXTS = new Set(['.opus']);
|
|
4
|
+
const FILE_TYPE_MAP = {
|
|
5
|
+
'.opus': 'opus', '.mp4': 'mp4', '.pdf': 'pdf',
|
|
6
|
+
'.doc': 'doc', '.docx': 'doc', '.xls': 'xls', '.xlsx': 'xls',
|
|
7
|
+
'.ppt': 'ppt', '.pptx': 'ppt',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* 根据文件扩展名检测媒体类型
|
|
11
|
+
*/
|
|
12
|
+
export function detectMediaKind(filePath) {
|
|
13
|
+
const dot = filePath.lastIndexOf('.');
|
|
14
|
+
if (dot === -1)
|
|
15
|
+
return 'file';
|
|
16
|
+
const ext = filePath.slice(dot).toLowerCase();
|
|
17
|
+
if (IMAGE_EXTS.has(ext))
|
|
18
|
+
return 'image';
|
|
19
|
+
if (AUDIO_EXTS.has(ext))
|
|
20
|
+
return 'audio';
|
|
21
|
+
return 'file';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 根据文件扩展名获取飞书 file_type 参数
|
|
25
|
+
*/
|
|
26
|
+
export function resolveFileType(filePath) {
|
|
27
|
+
const dot = filePath.lastIndexOf('.');
|
|
28
|
+
if (dot === -1)
|
|
29
|
+
return 'stream';
|
|
30
|
+
const ext = filePath.slice(dot).toLowerCase();
|
|
31
|
+
return FILE_TYPE_MAP[ext] ?? 'stream';
|
|
32
|
+
}
|
|
33
|
+
// ── Markdown 检测 ──
|
|
34
|
+
/**
|
|
35
|
+
* 检测文本是否包含 Markdown 语法
|
|
36
|
+
*
|
|
37
|
+
* 匹配常见 Markdown 元素:标题、粗体、斜体、代码块、链接、列表、引用、分割线、表格等。
|
|
38
|
+
* 短文本(<50 字符)且仅含单个 markdown 元素时倾向纯文本,避免误判。
|
|
39
|
+
*/
|
|
40
|
+
const MD_PATTERNS = [
|
|
41
|
+
/^#{1,6}\s/m, // 标题: # / ## / ###
|
|
42
|
+
/\*\*[^*]+\*\*/, // 粗体: **text**
|
|
43
|
+
/\*[^*]+\*/, // 斜体: *text*
|
|
44
|
+
/`[^`]+`/, // 行内代码: `code`
|
|
45
|
+
/```[\s\S]*?```/, // 代码块: ```...```
|
|
46
|
+
/\[.+?\]\(.+?\)/, // 链接: [text](url)
|
|
47
|
+
/^\s*[-*+]\s/m, // 无序列表: - item / * item
|
|
48
|
+
/^\s*\d+\.\s/m, // 有序列表: 1. item
|
|
49
|
+
/^\s*>/m, // 引用: > text
|
|
50
|
+
/^-{3,}$/m, // 分割线: ---
|
|
51
|
+
/^\|.+\|$/m, // 表格: | col | col |
|
|
52
|
+
];
|
|
53
|
+
export function isMarkdown(text) {
|
|
54
|
+
let matchCount = 0;
|
|
55
|
+
for (const pattern of MD_PATTERNS) {
|
|
56
|
+
if (pattern.test(text))
|
|
57
|
+
matchCount++;
|
|
58
|
+
if (matchCount >= 2)
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// 单个匹配:仅当文本较长时才认为是 markdown
|
|
62
|
+
return matchCount === 1 && text.length >= 80;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 将 markdown 内容转换为飞书交互式卡片格式
|
|
66
|
+
*/
|
|
67
|
+
function markdownToFeishuCard(content) {
|
|
68
|
+
return {
|
|
69
|
+
schema: '2.0',
|
|
70
|
+
config: { wide_screen_mode: true },
|
|
71
|
+
body: {
|
|
72
|
+
elements: [
|
|
73
|
+
{ tag: 'markdown', content },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 飞书通道适配器
|
|
80
|
+
*
|
|
81
|
+
* 将 FeishuSender 包装为 ChannelAdapter 接口。
|
|
82
|
+
* target 格式:
|
|
83
|
+
* - "chat_id" → sendText to chat
|
|
84
|
+
* - "reply/msg_id" → sendReply(向后兼容)
|
|
85
|
+
*/
|
|
86
|
+
export class FeishuAdapter {
|
|
87
|
+
scheme = 'feishu';
|
|
88
|
+
sender;
|
|
89
|
+
constructor(sender) {
|
|
90
|
+
this.sender = sender;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 将 mentions 注入到文本中(飞书 @ 格式)
|
|
94
|
+
*/
|
|
95
|
+
injectMentions(text, mentions) {
|
|
96
|
+
if (!mentions || mentions.length === 0)
|
|
97
|
+
return text;
|
|
98
|
+
const atTags = mentions.map(uid => `<at user_id="${uid}"></at>`).join(' ');
|
|
99
|
+
return `${atTags} ${text}`;
|
|
100
|
+
}
|
|
101
|
+
async send(target, message, options) {
|
|
102
|
+
const text = this.injectMentions(message, options?.mentions);
|
|
103
|
+
// 检测是否为 markdown 内容,且 sender 支持卡片发送
|
|
104
|
+
const useCard = isMarkdown(text) && !!this.sender.sendCard;
|
|
105
|
+
// 优先使用 options.replyTo
|
|
106
|
+
const replyTo = options?.replyTo;
|
|
107
|
+
// 向后兼容:target 以 "reply/" 开头
|
|
108
|
+
const resolvedReplyTo = replyTo || (target.startsWith('reply/') ? target.slice('reply/'.length) : undefined);
|
|
109
|
+
const chatTarget = target.startsWith('reply/') ? undefined : target;
|
|
110
|
+
// 构建日志上下文(task_id、reply 信息等)
|
|
111
|
+
const log = {
|
|
112
|
+
relatedTaskId: options?.taskId,
|
|
113
|
+
replyToMessageId: resolvedReplyTo,
|
|
114
|
+
replyInThread: options?.replyInThread,
|
|
115
|
+
};
|
|
116
|
+
if (useCard) {
|
|
117
|
+
const card = markdownToFeishuCard(text);
|
|
118
|
+
const id = await this.sender.sendCard({
|
|
119
|
+
receiveIdType: 'chat_id',
|
|
120
|
+
receiveId: chatTarget ?? '',
|
|
121
|
+
card,
|
|
122
|
+
replyToMessageId: resolvedReplyTo,
|
|
123
|
+
log: { ...log, contentOverride: text },
|
|
124
|
+
});
|
|
125
|
+
return { id };
|
|
126
|
+
}
|
|
127
|
+
// 纯文本发送
|
|
128
|
+
if (resolvedReplyTo) {
|
|
129
|
+
const id = await this.sender.sendReply({
|
|
130
|
+
text,
|
|
131
|
+
replyToMessageId: resolvedReplyTo,
|
|
132
|
+
replyInThread: options?.replyInThread,
|
|
133
|
+
chatId: chatTarget,
|
|
134
|
+
log,
|
|
135
|
+
});
|
|
136
|
+
return { id };
|
|
137
|
+
}
|
|
138
|
+
const id = await this.sender.sendText({
|
|
139
|
+
receiveIdType: 'chat_id',
|
|
140
|
+
receiveId: chatTarget ?? target,
|
|
141
|
+
text,
|
|
142
|
+
log,
|
|
143
|
+
});
|
|
144
|
+
return { id };
|
|
145
|
+
}
|
|
146
|
+
async sendFile(target, file, message, options) {
|
|
147
|
+
const path = await import('node:path');
|
|
148
|
+
const fileName = file.fileName || path.basename(file.filePath);
|
|
149
|
+
// 确定回复目标
|
|
150
|
+
const replyTo = options?.replyTo || (target.startsWith('reply/') ? target.slice('reply/'.length) : undefined);
|
|
151
|
+
const chatTarget = target.startsWith('reply/') ? undefined : target;
|
|
152
|
+
// 自动检测媒体类型
|
|
153
|
+
const mediaKind = detectMediaKind(file.filePath);
|
|
154
|
+
let mediaMessageId;
|
|
155
|
+
if (mediaKind === 'image') {
|
|
156
|
+
// 图片:走 image.create 上传 → image 消息
|
|
157
|
+
if (!this.sender.uploadImage || !this.sender.sendImage) {
|
|
158
|
+
throw new Error('FeishuSender 未实现图片上传能力');
|
|
159
|
+
}
|
|
160
|
+
const imageKey = await this.sender.uploadImage(file.filePath);
|
|
161
|
+
mediaMessageId = await this.sender.sendImage({
|
|
162
|
+
receiveIdType: 'chat_id',
|
|
163
|
+
receiveId: chatTarget ?? '',
|
|
164
|
+
imageKey,
|
|
165
|
+
replyToMessageId: replyTo,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// 文件/音频:走 file.create 上传 → file/audio 消息
|
|
170
|
+
if (!this.sender.uploadFile || !this.sender.sendFile) {
|
|
171
|
+
throw new Error('FeishuSender 未实现文件上传能力');
|
|
172
|
+
}
|
|
173
|
+
const fileType = file.fileType || resolveFileType(file.filePath);
|
|
174
|
+
const fileKey = await this.sender.uploadFile({
|
|
175
|
+
filePath: file.filePath,
|
|
176
|
+
fileName,
|
|
177
|
+
fileType,
|
|
178
|
+
duration: file.duration,
|
|
179
|
+
});
|
|
180
|
+
const msgType = mediaKind === 'audio' ? 'audio' : 'file';
|
|
181
|
+
mediaMessageId = await this.sender.sendFile({
|
|
182
|
+
receiveIdType: 'chat_id',
|
|
183
|
+
receiveId: chatTarget ?? '',
|
|
184
|
+
fileKey,
|
|
185
|
+
msgType,
|
|
186
|
+
replyToMessageId: replyTo,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// 如果有附带文本,额外发一条文本消息
|
|
190
|
+
if (message) {
|
|
191
|
+
const text = this.injectMentions(message, options?.mentions);
|
|
192
|
+
const log = { relatedTaskId: options?.taskId, replyToMessageId: replyTo };
|
|
193
|
+
if (replyTo) {
|
|
194
|
+
await this.sender.sendReply({ text, replyToMessageId: replyTo, chatId: chatTarget, log });
|
|
195
|
+
}
|
|
196
|
+
else if (chatTarget) {
|
|
197
|
+
await this.sender.sendText({ receiveIdType: 'chat_id', receiveId: chatTarget, text, log });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { id: mediaMessageId };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=feishu-adapter.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道适配器接口
|
|
3
|
+
*
|
|
4
|
+
* 统一飞书/Slack/Web/CLI 等不同通道的消息发送。
|
|
5
|
+
* channel.send 根据 channel 字段路由到对应 adapter。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 通道注册表
|
|
9
|
+
*
|
|
10
|
+
* 管理所有已注册的通道适配器。
|
|
11
|
+
* channel.send 通过 channel 字段查找对应的 adapter。
|
|
12
|
+
*/
|
|
13
|
+
export class ChannelRegistry {
|
|
14
|
+
adapters = new Map();
|
|
15
|
+
register(adapter) {
|
|
16
|
+
this.adapters.set(adapter.scheme, adapter);
|
|
17
|
+
}
|
|
18
|
+
get(scheme) {
|
|
19
|
+
return this.adapters.get(scheme);
|
|
20
|
+
}
|
|
21
|
+
has(scheme) {
|
|
22
|
+
return this.adapters.has(scheme);
|
|
23
|
+
}
|
|
24
|
+
getSchemes() {
|
|
25
|
+
return Array.from(this.adapters.keys());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
/** 默认通道(当前只支持飞书) */
|
|
3
|
+
const DEFAULT_SCHEME = 'feishu';
|
|
4
|
+
/**
|
|
5
|
+
* 向飞书群发消息
|
|
6
|
+
*
|
|
7
|
+
* target 直接填群聊 ID(如 oc_xxx)。
|
|
8
|
+
* 支持发送文件、@ 用户和回复消息。
|
|
9
|
+
*/
|
|
10
|
+
export async function channelSend(deps, input) {
|
|
11
|
+
const { db, channelRegistry, logger } = deps;
|
|
12
|
+
const scheme = DEFAULT_SCHEME;
|
|
13
|
+
const chatTarget = input.target;
|
|
14
|
+
const adapter = channelRegistry.get(scheme);
|
|
15
|
+
if (!adapter) {
|
|
16
|
+
const error = `通道 ${scheme} 未注册(已注册: ${channelRegistry.getSchemes().join(', ')})`;
|
|
17
|
+
logger?.error(`[anya:pipeline] channel.send: ${error}`);
|
|
18
|
+
return { sent: false, error };
|
|
19
|
+
}
|
|
20
|
+
const sendOptions = {
|
|
21
|
+
replyTo: input.reply_to,
|
|
22
|
+
replyInThread: input.reply_in_thread,
|
|
23
|
+
mentions: input.mentions,
|
|
24
|
+
taskId: input.task_id,
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
let result;
|
|
28
|
+
if (input.file) {
|
|
29
|
+
if (!adapter.sendFile) {
|
|
30
|
+
const error = `通道 ${scheme} 不支持发送文件`;
|
|
31
|
+
logger?.error(`[anya:pipeline] channel.send: ${error}`);
|
|
32
|
+
return { sent: false, error };
|
|
33
|
+
}
|
|
34
|
+
result = await adapter.sendFile(chatTarget, {
|
|
35
|
+
filePath: input.file.file_path,
|
|
36
|
+
fileName: input.file.file_name,
|
|
37
|
+
fileType: input.file.file_type,
|
|
38
|
+
duration: input.file.duration,
|
|
39
|
+
}, input.message, sendOptions);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
result = await adapter.send(chatTarget, input.message, sendOptions);
|
|
43
|
+
}
|
|
44
|
+
// message_log 由底层 sender 方法自动记录(含 source_ref、metadata 等)
|
|
45
|
+
// 审计日志
|
|
46
|
+
insertAuditEvent(db, {
|
|
47
|
+
event_type: 'channel_send',
|
|
48
|
+
actor: 'loid',
|
|
49
|
+
task_id: input.task_id ?? undefined,
|
|
50
|
+
summary: `通过 ${scheme} 发消息: ${input.message.slice(0, 100)}`,
|
|
51
|
+
detail: JSON.stringify({
|
|
52
|
+
target: chatTarget,
|
|
53
|
+
has_file: !!input.file,
|
|
54
|
+
has_mentions: !!input.mentions?.length,
|
|
55
|
+
reply_to: input.reply_to ?? null,
|
|
56
|
+
sent: true,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
deps.onMessageSent?.(chatTarget);
|
|
60
|
+
logger?.info(`[anya:pipeline] [Loid] channel.send (${scheme}${input.file ? ', file' : ''}): "${input.message.slice(0, 80)}"`);
|
|
61
|
+
return { sent: true, message_id: result.id };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
logger?.error(`[anya:pipeline] channel.send 失败:`, err);
|
|
65
|
+
insertAuditEvent(db, {
|
|
66
|
+
event_type: 'channel_send_failed',
|
|
67
|
+
actor: 'loid',
|
|
68
|
+
task_id: input.task_id ?? undefined,
|
|
69
|
+
summary: `通道发送失败: ${String(err)}`,
|
|
70
|
+
detail: JSON.stringify({ target: chatTarget, error: String(err) }),
|
|
71
|
+
});
|
|
72
|
+
return { sent: false, error: String(err) };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=channel-send.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { insertAuditEvent } from '@team-anya/db';
|
|
2
|
+
/**
|
|
3
|
+
* file.upload - 通用文件上传
|
|
4
|
+
*
|
|
5
|
+
* 与通道无关。当前使用本地文件复制实现。
|
|
6
|
+
* 上传到 $ANYA_HOME/uploads/ 目录,返回本地路径作为 URL。
|
|
7
|
+
*/
|
|
8
|
+
export async function fileUpload(deps, input) {
|
|
9
|
+
const fs = await import('node:fs');
|
|
10
|
+
const path = await import('node:path');
|
|
11
|
+
const srcPath = input.file_path;
|
|
12
|
+
const fileName = input.file_name || path.basename(srcPath);
|
|
13
|
+
// 检查源文件是否存在
|
|
14
|
+
if (!fs.existsSync(srcPath)) {
|
|
15
|
+
throw new Error(`文件不存在: ${srcPath}`);
|
|
16
|
+
}
|
|
17
|
+
const stat = fs.statSync(srcPath);
|
|
18
|
+
if (!stat.isFile()) {
|
|
19
|
+
throw new Error(`路径不是文件: ${srcPath}`);
|
|
20
|
+
}
|
|
21
|
+
// 确保上传目录存在
|
|
22
|
+
fs.mkdirSync(deps.uploadDir, { recursive: true });
|
|
23
|
+
// 生成唯一文件名避免冲突
|
|
24
|
+
const timestamp = Date.now();
|
|
25
|
+
const uniqueName = `${timestamp}-${fileName}`;
|
|
26
|
+
const destPath = path.join(deps.uploadDir, uniqueName);
|
|
27
|
+
// 复制文件
|
|
28
|
+
fs.copyFileSync(srcPath, destPath);
|
|
29
|
+
const url = destPath; // 本地路径即 URL
|
|
30
|
+
deps.logger?.info(`[anya:pipeline] file.upload: ${fileName} (${stat.size} bytes) → ${destPath}`);
|
|
31
|
+
// 审计日志
|
|
32
|
+
insertAuditEvent(deps.db, {
|
|
33
|
+
event_type: 'file_upload',
|
|
34
|
+
actor: 'loid',
|
|
35
|
+
summary: `上传文件: ${fileName} (${stat.size} bytes)`,
|
|
36
|
+
detail: JSON.stringify({ src: srcPath, dest: destPath, size: stat.size }),
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
url,
|
|
40
|
+
file_name: fileName,
|
|
41
|
+
file_size: stat.size,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=file-upload.js.map
|