@zhin.js/adapter-milky 5.0.0 → 5.0.1
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/CHANGELOG.md +58 -0
- package/adapters/milky.ts +11 -0
- package/commands/endpoint/add/[name:string].ts +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[name:string].ts +3 -0
- package/lib/endpoint-management.d.ts +17 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/milky-endpoint-commands.d.ts +1 -0
- package/lib/milky-endpoint-commands.js +18 -0
- package/lib/milky-runtime-state.d.ts +1 -0
- package/lib/milky-runtime-state.js +6 -0
- package/lib/protocol.d.ts +8 -0
- package/lib/protocol.js +121 -0
- package/lib/sse-endpoint.d.ts +2 -1
- package/lib/sse-endpoint.js +51 -42
- package/lib/webhook-endpoint.d.ts +2 -1
- package/lib/webhook-endpoint.js +5 -1
- package/lib/ws-endpoint.d.ts +2 -1
- package/lib/ws-endpoint.js +51 -57
- package/lib/wss-endpoint.d.ts +2 -1
- package/lib/wss-endpoint.js +5 -1
- package/package.json +18 -12
- package/plugin.ts +6 -0
- package/src/endpoint-management.ts +74 -0
- package/src/milky-endpoint-commands.ts +19 -0
- package/src/milky-runtime-state.ts +7 -0
- package/src/protocol.ts +120 -0
- package/src/sse-endpoint.ts +52 -41
- package/src/webhook-endpoint.ts +6 -1
- package/src/ws-endpoint.ts +53 -55
- package/src/wss-endpoint.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# @zhin.js/adapter-milky
|
|
2
2
|
|
|
3
|
+
## 5.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- cdf64e7: 多方向审计修复批(8 面 30+ bug):
|
|
8
|
+
|
|
9
|
+
- **安全**:钉钉 webhook 验签绕过修复(缺 timestamp/sign 一律 403 + ±1h 防重放);exec-policy fail-closed(换行/`$(`/反引号拒绝、管道逐段过白名单、env dump 复合拆段);wecom 验签改 timingSafeEqual;config:set 拒绝 `__proto__` 等魔术键、host 键优先防覆写。
|
|
10
|
+
- **P0 功能**:`zhin packages` scoped 包名解析为空导致 rm -rf 风险;命令数字参数解析失败炸断消息链路(dispatch 捕获 continue);TaskQueue 监听器首个事件自摘除导致 assistant 队列挂死(+超时后完成不覆盖终态);DatabaseHost 跨世代共享崩溃(define 幂等 + stop 改进程级);rss 按不存在 id 列删除改业务键。
|
|
11
|
+
- **Runtime/HMR**:native watcher 过滤忽略目录(lib/.zhin 不再触发重载风暴);host 段配置 patch(http.port 等)存在 installResources 时全量重建;capability 目录非 entry 支持文件升级进程重启;documentTransaction 失败回滚。
|
|
12
|
+
- **CLI 写侧**:config/setup 对 toml 静默假成功改统一 config-file(不支持格式报错);doctor/onboard 默认配置改新形态 plugins 映射;schedule add 改 --prompt;migrate engines/中文模板误伤/覆盖前备份。
|
|
13
|
+
- **适配器生命周期**:napcat/milky start 失败竞态与僵尸连接、心跳 close 清理、stop-during-connect settle;slack webhook 二次 writeHead + messageChannelMap LRU。
|
|
14
|
+
- **Host/向导**:logs/stats 与 inbox 查询改 DB 侧 count/orderBy/limit 下推;save-yaml 写入前校验;zhipu/moonshot baseUrl 必填预填;.env 写入转义 + 幂等合并;setup 数据库密码不再明文落 config;60s fetch 超时与 JSON 守卫。
|
|
15
|
+
|
|
16
|
+
- 2d0a159: 审计尾账清零(P2 批):
|
|
17
|
+
|
|
18
|
+
- Runtime:reload 前重读配置文档消除陈旧快照;组合式根 schema 显式报错(不再静默空 view);ConfigPatch 支持数组数字索引(`endpoints.0.url`);console 配置读写单一数据源 + 写入串行化;watch tick 防重叠 + fetch 超时。
|
|
19
|
+
- Host/MCP:MCP client stop 成功才标记 + handoff 补 quiescePrevious/resumePrevious(独占端口不再新旧并存);readJsonBody 超限保留连接回 413;dispatchHttp 统一 HttpBodyError 状态码;inbox endpoint 名仅命中才缓存;create_plugin 工具生成物改 definePlugin 新格式。
|
|
20
|
+
- Agent:CapabilityIngress 按 projection 归属记账(key 振荡不再泄漏/误 purge);敏感目录 `data` 锚定工作区根(src/data 不再误伤);passive-group-buffer 死 key 清扫;tool scopes 数组校验。
|
|
21
|
+
- 插件:blackjack 终局「回复 1」复活(getLatestForUser);60s apiBase 改运行时求值(弃 process.env);rss \_db lifecycle 清理;group-suite flush 不丢计数 + checkin 串行化防双签;milky SSE start 失败复位。
|
|
22
|
+
- 工具链:applyAdaptersToConfig 改合并(重跑 wizard 不丢手工 endpoint);html-renderer 提示识别 plugins 映射;create-zhin CLI 项目名校验 + task XML/NSSM 修复;setup --ai 补 @zhin.js/tool;layout 发现支持 .ts。
|
|
23
|
+
|
|
24
|
+
- 078e3f7: 架构统一批(AURA):
|
|
25
|
+
|
|
26
|
+
- **EndpointLifecycle 基座**(@zhin.js/adapter 新增 `createEndpointLifecycle`):WS/SSE 端点的 start 失败复位、仅曾 open 才退避重连(指数+jitter 可配)、stop 不重连、PONG 看门狗、定时器集中清理、陈旧事件防叠套;napcat/milky/onebot11/onebot12/satori 已迁移(删除各自手写状态机),从此同类竞态在结构上不可能再犯。
|
|
27
|
+
- **Generation-store**(@zhin.js/plugin-runtime 新增 `createGenerationStore`):模块级运行时状态的一等能力,provide 自动挂 lifecycle 反注册(代际结束自动清理);lottery deps 与 rss db 已迁移,公开 API 兼容。
|
|
28
|
+
- **Resolver 管线收敛**(@zhin.js/runtime):解析规则统一为 local path → workspace → node_modules 单管线;optional 引用对所有 PackageResolutionError 容错(消除 message 前缀补丁)。
|
|
29
|
+
- **工具目录准入统一**(@zhin.js/agent):RegisteredToolSource 与 ExternalToolSource 共用同一 `canAccessTool` 准入(platforms/scopes/permissions/hidden 四元组全链路透传),同名覆盖 warn;AgentToolRegistration 补 platforms/scopes。两条注册通道(静态约定 vs 动态注册)职责边界已文档化。
|
|
30
|
+
|
|
31
|
+
- 9c997b2: 通用 endpoint 管理命令套件:`@zhin.js/adapter` 新增 `createEndpointCommands(spec, defineCommand)`——`<adapter> endpoint list / add <name> key=value... / remove <name>` 三件套,含 kv 解析、`.env` 凭据派生(`<ADAPTER>_<NAME>_<FIELD>`)、yaml 写回保留注释、master 权限门禁(通用 `isEndpointOperator`)、自定义 bindFlow 钩子。QQ 迁移至套件(行为与扫码绑定流程不变);napcat / onebot11 / onebot12 / milky / slack / telegram 接入(字段对齐各自 schema,features 补 @zhin.js/command)。
|
|
32
|
+
- 09d4f25: Console 社交读取面(management 语义端口)多平台落地:napcat/onebot11/onebot12/milky(好友+群+群成员,OneBot 标准动作);discord/kook/satori(guild+频道+成员,分页聚合,id 保精度留字符串);slack(workspace 成员+public channels+conversations.members);line(群/room 成员分页+profile 回退);wechat-mp(followers openid);weixin-ilink(context_token 对端推导);lark(chats+members 全分页)。`EndpointFriend.user_id`/`EndpointGroup.group_id` 放宽为 `number | string`(雪花 id 不丢精度)。telegram/wecom/dingtalk/github/email/sandbox 注明平台无列表面暂不接。
|
|
33
|
+
- 6cb6152: 统一消息元素通道(UNI-Channel)落地:
|
|
34
|
+
|
|
35
|
+
- **入站契约**:`IncomingMessage.segments`(canonical Segment[],与 content 纯文本视图同源双轨),Message 透传;AI 兜底链路经 `collectSegmentMedia` 把图片/语音/视频/文件 MediaRef 写入会话 extra——多模态输入不再丢失。
|
|
36
|
+
- **出站协商**:`normalizeOutboundPayload` 升级全量 canonical 归一(复用 generic-segment-mapper),html→image 按端点 `segments.outboundMedia` 声明降级(base64 直发 / url-or-text / passthrough 自行物化);`MediaRef.kind` 新增 `'file'` 承载平台不透明引用(file_id/resource_id)。
|
|
37
|
+
- **能力声明**:`defineAdapter.segments` policy(outboundMedia / interactive),三道段门禁复活(探测点改 adapters/\*.ts,豁免名单渐进收敛)。
|
|
38
|
+
- **首批迁移**:icqq 全保真出入站(CQ ↔ canonical,quote→reply 段);milky/telegram/discord 入站媒体段恢复(附件/贴纸/callback action);napcat/onebot11/onebot12 出站 canonical→OneBot 数组段;wechat-mp/wecom `/cgi-bin/media/upload` 与 lark `/im/v1/images` 上传通路(base64/URL 图片不再静默丢图,失败降级文本)。
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [cdf64e7]
|
|
41
|
+
- Updated dependencies [2d0a159]
|
|
42
|
+
- Updated dependencies [5691aba]
|
|
43
|
+
- Updated dependencies [078e3f7]
|
|
44
|
+
- Updated dependencies [50497a5]
|
|
45
|
+
- Updated dependencies [9c997b2]
|
|
46
|
+
- Updated dependencies [09d4f25]
|
|
47
|
+
- Updated dependencies [43485a9]
|
|
48
|
+
- Updated dependencies [f0ec5ab]
|
|
49
|
+
- Updated dependencies [3e925d0]
|
|
50
|
+
- Updated dependencies [fa66c4c]
|
|
51
|
+
- Updated dependencies [fa66c4c]
|
|
52
|
+
- Updated dependencies [6cb6152]
|
|
53
|
+
- @zhin.js/command@1.0.3
|
|
54
|
+
- @zhin.js/agent@1.0.6
|
|
55
|
+
- @zhin.js/plugin-runtime@1.1.1
|
|
56
|
+
- @zhin.js/host-http@1.0.3
|
|
57
|
+
- zhin.js@5.0.1
|
|
58
|
+
- @zhin.js/adapter@1.1.1
|
|
59
|
+
- @zhin.js/core@1.4.1
|
|
60
|
+
|
|
3
61
|
## 5.0.0
|
|
4
62
|
|
|
5
63
|
### Patch Changes
|
package/adapters/milky.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolveMilkyConfig,
|
|
15
15
|
type MilkyAdapterConfig,
|
|
16
16
|
} from '../src/protocol.js';
|
|
17
|
+
import { milkyRuntimeStateToken } from '../src/milky-runtime-state.js';
|
|
17
18
|
|
|
18
19
|
export {
|
|
19
20
|
MilkySseEndpoint,
|
|
@@ -33,9 +34,19 @@ export type {
|
|
|
33
34
|
|
|
34
35
|
export default defineAdapter<MilkyAdapterConfig>({
|
|
35
36
|
capabilities: ['inbound', 'outbound'],
|
|
37
|
+
// Milky 协议资源 uri 消费 http(s):// 与 base64:// 形式;无卡片交互面,交互段降级纯文本。
|
|
38
|
+
segments: {
|
|
39
|
+
outboundMedia: ['url', 'base64'],
|
|
40
|
+
interactive: 'text',
|
|
41
|
+
},
|
|
36
42
|
create(context) {
|
|
37
43
|
const config = resolveMilkyConfig(context.config);
|
|
38
44
|
const gateway = context.use(messageGatewayToken);
|
|
45
|
+
// 注册到插件运行时状态(milky endpoint list 的"运行中"数据源)
|
|
46
|
+
context.use(milkyRuntimeStateToken).endpoints.set(config.name, {
|
|
47
|
+
name: config.name,
|
|
48
|
+
mode: config.connection,
|
|
49
|
+
});
|
|
39
50
|
if (config.connection === 'webhook') {
|
|
40
51
|
return new MilkyWebhookEndpoint({
|
|
41
52
|
id: context.id,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milky endpoint management 语义端口(Console 社交面 RPC 消费,见
|
|
3
|
+
* packages/im/adapter/src/endpoint-management.ts)。
|
|
4
|
+
*
|
|
5
|
+
* Milky 协议响应 data 为包裹对象:get_friend_list → {friends:[...]}、
|
|
6
|
+
* get_group_list → {groups:[...]}、get_group_member_list → {members:[...]}。
|
|
7
|
+
* 归一化取舍:
|
|
8
|
+
* - friend → {user_id:number, nickname, remark: remark ?? ''}
|
|
9
|
+
* - group → {group_id:number, name: group_name ?? name}
|
|
10
|
+
* - 群成员列表保持 Milky 原生形状,仅保证数组
|
|
11
|
+
*/
|
|
12
|
+
import type { EndpointManagement } from '@zhin.js/adapter';
|
|
13
|
+
/** 管理面只依赖 endpoint 的 callApi(ws/wss/sse/webhook 四种传输各自实现)。 */
|
|
14
|
+
export interface MilkyManagementCaller {
|
|
15
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createMilkyEndpointManagement(endpoint: MilkyManagementCaller): EndpointManagement;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function createMilkyEndpointManagement(endpoint) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
async listFriends() {
|
|
4
|
+
const data = asRecord(await endpoint.callApi('get_friend_list'));
|
|
5
|
+
return toArray(data.friends).map((value) => {
|
|
6
|
+
const friend = asRecord(value);
|
|
7
|
+
return {
|
|
8
|
+
user_id: toNumberId(friend.user_id, 'user_id'),
|
|
9
|
+
nickname: String(friend.nickname ?? ''),
|
|
10
|
+
remark: String(friend.remark ?? ''),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
async listGroups() {
|
|
15
|
+
const data = asRecord(await endpoint.callApi('get_group_list'));
|
|
16
|
+
return toArray(data.groups).map((value) => {
|
|
17
|
+
const group = asRecord(value);
|
|
18
|
+
return {
|
|
19
|
+
group_id: toNumberId(group.group_id, 'group_id'),
|
|
20
|
+
name: String(group.group_name ?? group.name ?? ''),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
async listGroupMembers(groupId) {
|
|
25
|
+
const data = asRecord(await endpoint.callApi('get_group_member_list', {
|
|
26
|
+
group_id: toNumberId(groupId, 'group_id'),
|
|
27
|
+
}));
|
|
28
|
+
return toArray(data.members);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function asRecord(value) {
|
|
33
|
+
return value !== null && typeof value === 'object'
|
|
34
|
+
? value
|
|
35
|
+
: {};
|
|
36
|
+
}
|
|
37
|
+
function toArray(value) {
|
|
38
|
+
return Array.isArray(value) ? value : [];
|
|
39
|
+
}
|
|
40
|
+
/** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
|
|
41
|
+
function toNumberId(value, label) {
|
|
42
|
+
const n = Number(value);
|
|
43
|
+
if (!Number.isFinite(n) || String(value ?? '').trim() === '') {
|
|
44
|
+
throw new TypeError(`milky ${label} 必须是数字: ${String(value)}`);
|
|
45
|
+
}
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const milkyEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `milky endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
|
+
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
|
+
*/
|
|
5
|
+
import { createEndpointCommands } from '@zhin.js/adapter';
|
|
6
|
+
import { defineCommand } from '@zhin.js/command';
|
|
7
|
+
import { milkyRuntimeStateToken } from './milky-runtime-state.js';
|
|
8
|
+
export const milkyEndpointCommands = createEndpointCommands({
|
|
9
|
+
adapterKey: 'milky',
|
|
10
|
+
adapterDisplayName: 'Milky',
|
|
11
|
+
fields: [
|
|
12
|
+
{ key: 'baseUrl', required: true, description: 'Milky HTTP API base URL' },
|
|
13
|
+
{ key: 'path', description: 'webhook / reverse-wss 路径' },
|
|
14
|
+
{ key: 'access_token', env: true, description: 'Milky access token' },
|
|
15
|
+
],
|
|
16
|
+
running: (use) => use(milkyRuntimeStateToken).endpoints.values(),
|
|
17
|
+
describeEntry: (entry) => `baseUrl: ${String(entry.baseUrl)}`,
|
|
18
|
+
}, defineCommand);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const milkyRuntimeStateToken: import("@zhin.js/plugin-runtime").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milky 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `milky endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
|
|
6
|
+
export const milkyRuntimeStateToken = defineEndpointRuntimeStateToken('milky');
|
package/lib/protocol.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
* Spec: https://milky.ntqqrev.org/
|
|
5
5
|
*/
|
|
6
|
+
import type { Segment } from '@zhin.js/core/runtime';
|
|
6
7
|
/** Transitional legacy endpoint row (`endpoints[]` with `context: milky`). */
|
|
7
8
|
export interface MilkyLegacyEndpointRow {
|
|
8
9
|
readonly context?: string;
|
|
@@ -124,6 +125,13 @@ export declare function isMessageReceiveEvent(event: MilkyEvent): event is Milky
|
|
|
124
125
|
/** Gateway reply target:`private:uid` / `group:gid` */
|
|
125
126
|
export declare function formatInboundTarget(data: MilkyIncomingMessage): string;
|
|
126
127
|
export declare function formatInboundContent(data: MilkyIncomingMessage): string;
|
|
128
|
+
/**
|
|
129
|
+
* 入站消息段 → canonical Segment[](与 formatInboundContent 纯文本视图同源双轨)。
|
|
130
|
+
* 媒体段(image/record/video)优先 temp_url(kind=url),缺 URL 时回退
|
|
131
|
+
* resource_id(kind=file,平台不透明引用,经协议端 get_resource 解析);
|
|
132
|
+
* file 段只有 file_id(kind=file)。未识别段类型按宽松段透传,不丢信息。
|
|
133
|
+
*/
|
|
134
|
+
export declare function formatInboundSegments(data: MilkyIncomingMessage): Segment[];
|
|
127
135
|
/** First audio URI from inbound segments (for Agent Host STT preprocess). */
|
|
128
136
|
export declare function extractInboundAudioUrl(data: MilkyIncomingMessage): string | undefined;
|
|
129
137
|
/** 发送者显示名(群名片/群昵称/好友昵称);没有可靠的显示名时返回 undefined。 */
|
package/lib/protocol.js
CHANGED
|
@@ -138,6 +138,127 @@ export function formatInboundContent(data) {
|
|
|
138
138
|
return '';
|
|
139
139
|
}).join('');
|
|
140
140
|
}
|
|
141
|
+
function firstNonEmptyString(...values) {
|
|
142
|
+
for (const value of values) {
|
|
143
|
+
if (typeof value === 'string' && value.trim())
|
|
144
|
+
return value.trim();
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 入站消息段 → canonical Segment[](与 formatInboundContent 纯文本视图同源双轨)。
|
|
150
|
+
* 媒体段(image/record/video)优先 temp_url(kind=url),缺 URL 时回退
|
|
151
|
+
* resource_id(kind=file,平台不透明引用,经协议端 get_resource 解析);
|
|
152
|
+
* file 段只有 file_id(kind=file)。未识别段类型按宽松段透传,不丢信息。
|
|
153
|
+
*/
|
|
154
|
+
export function formatInboundSegments(data) {
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const seg of data.segments) {
|
|
157
|
+
const segData = seg.data ?? {};
|
|
158
|
+
switch (seg.type) {
|
|
159
|
+
case 'text': {
|
|
160
|
+
const text = String(segData.text ?? '');
|
|
161
|
+
if (text)
|
|
162
|
+
out.push({ type: 'text', data: { text } });
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'mention':
|
|
166
|
+
out.push({
|
|
167
|
+
type: 'mention',
|
|
168
|
+
data: {
|
|
169
|
+
target: String(segData.user_id ?? ''),
|
|
170
|
+
...(typeof segData.name === 'string' && segData.name
|
|
171
|
+
? { name: segData.name }
|
|
172
|
+
: {}),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
break;
|
|
176
|
+
case 'mention_all':
|
|
177
|
+
out.push({ type: 'mention', data: { target: 'all' } });
|
|
178
|
+
break;
|
|
179
|
+
case 'face': {
|
|
180
|
+
const id = firstNonEmptyString(segData.face_id, segData.id);
|
|
181
|
+
if (id)
|
|
182
|
+
out.push({ type: 'face', data: { id } });
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'reply': {
|
|
186
|
+
const seq = Number(segData.message_seq);
|
|
187
|
+
if (Number.isFinite(seq)) {
|
|
188
|
+
// 与 formatInboundMessageId 同一复合格式,保证 reply 链可定位原消息
|
|
189
|
+
out.push({
|
|
190
|
+
type: 'reply',
|
|
191
|
+
data: { message_id: `${data.message_scene}:${data.peer_id}:${seq}` },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'image':
|
|
197
|
+
case 'record':
|
|
198
|
+
case 'audio':
|
|
199
|
+
case 'video': {
|
|
200
|
+
const url = firstNonEmptyString(segData.temp_url, segData.uri, segData.url);
|
|
201
|
+
const resourceId = firstNonEmptyString(segData.resource_id);
|
|
202
|
+
if (!url && !resourceId)
|
|
203
|
+
break;
|
|
204
|
+
const type = seg.type === 'image'
|
|
205
|
+
? 'image'
|
|
206
|
+
: seg.type === 'video'
|
|
207
|
+
? 'video'
|
|
208
|
+
: 'audio';
|
|
209
|
+
out.push({
|
|
210
|
+
type,
|
|
211
|
+
data: {
|
|
212
|
+
media: url
|
|
213
|
+
? { kind: 'url', value: url }
|
|
214
|
+
: { kind: 'file', value: resourceId },
|
|
215
|
+
...(typeof segData.summary === 'string' && segData.summary
|
|
216
|
+
? { alt: segData.summary }
|
|
217
|
+
: {}),
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'file': {
|
|
223
|
+
const fileId = firstNonEmptyString(segData.file_id);
|
|
224
|
+
if (!fileId)
|
|
225
|
+
break;
|
|
226
|
+
out.push({
|
|
227
|
+
type: 'file',
|
|
228
|
+
data: {
|
|
229
|
+
media: { kind: 'file', value: fileId },
|
|
230
|
+
...(typeof segData.file_name === 'string' && segData.file_name
|
|
231
|
+
? { name: segData.file_name }
|
|
232
|
+
: {}),
|
|
233
|
+
...(typeof segData.file_size === 'number'
|
|
234
|
+
? { size: segData.file_size }
|
|
235
|
+
: {}),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'forward': {
|
|
241
|
+
const forwardId = firstNonEmptyString(segData.forward_id);
|
|
242
|
+
if (forwardId) {
|
|
243
|
+
out.push({
|
|
244
|
+
type: 'forward',
|
|
245
|
+
data: {
|
|
246
|
+
forward_id: forwardId,
|
|
247
|
+
...(typeof segData.title === 'string' && segData.title
|
|
248
|
+
? { title: segData.title }
|
|
249
|
+
: {}),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
default:
|
|
256
|
+
// 未识别段(market_face / light_app / xml 等)按宽松 canonical 段透传
|
|
257
|
+
out.push({ type: seg.type, data: segData });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
141
262
|
/** First audio URI from inbound segments (for Agent Host STT preprocess). */
|
|
142
263
|
export function extractInboundAudioUrl(data) {
|
|
143
264
|
for (const seg of data.segments) {
|
package/lib/sse-endpoint.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Milky SSE client endpoint — GET text/event-stream on /event.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type EndpointInstance, type EndpointManagement } from '@zhin.js/adapter';
|
|
5
5
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
6
6
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
7
7
|
import { callApi, type MilkyEvent, type MilkySseConfig } from './protocol.js';
|
|
@@ -22,6 +22,7 @@ export interface MilkySseEndpointOptions {
|
|
|
22
22
|
}
|
|
23
23
|
export declare class MilkySseEndpoint implements EndpointInstance {
|
|
24
24
|
#private;
|
|
25
|
+
readonly management: EndpointManagement;
|
|
25
26
|
constructor(options: MilkySseEndpointOptions);
|
|
26
27
|
start(): Promise<void>;
|
|
27
28
|
open(): void;
|
package/lib/sse-endpoint.js
CHANGED
|
@@ -1,28 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Milky SSE client endpoint — GET text/event-stream on /event.
|
|
3
|
+
*/
|
|
4
|
+
import { createEndpointLifecycle, } from '@zhin.js/adapter';
|
|
1
5
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
|
+
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
2
7
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
3
|
-
import { buildSendAction, buildSseConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
8
|
+
import { buildSendAction, buildSseConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
4
9
|
import { openSseStream } from './sse-client.js';
|
|
5
10
|
const logger = getLogger('milky');
|
|
6
11
|
export class MilkySseEndpoint {
|
|
7
12
|
#options;
|
|
8
13
|
#callApi;
|
|
14
|
+
management = createMilkyEndpointManagement(this);
|
|
15
|
+
#lifecycle;
|
|
9
16
|
#stream;
|
|
10
|
-
#reconnectTimer;
|
|
11
17
|
#open = false;
|
|
12
|
-
#started = false;
|
|
13
|
-
#stopping = false;
|
|
14
18
|
#unregisterAgent;
|
|
15
19
|
constructor(options) {
|
|
16
20
|
this.#options = options;
|
|
17
21
|
this.#callApi = options.callApi ?? callApi;
|
|
22
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
23
|
+
name: options.config.name,
|
|
24
|
+
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
25
|
+
reconnect: {
|
|
26
|
+
initialIntervalMs: options.config.reconnect_interval,
|
|
27
|
+
multiplier: 1,
|
|
28
|
+
maxIntervalMs: Number.MAX_SAFE_INTEGER,
|
|
29
|
+
jitterMs: 0,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
18
32
|
}
|
|
19
33
|
async start() {
|
|
20
|
-
if (this.#started)
|
|
34
|
+
if (this.#lifecycle.started)
|
|
21
35
|
return;
|
|
22
|
-
this.#started = true;
|
|
23
|
-
this.#stopping = false;
|
|
24
36
|
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
|
|
25
|
-
|
|
37
|
+
try {
|
|
38
|
+
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
// 与 WS 对齐:start 失败复位由基座保证;agent 反注册留在适配器侧,允许重试
|
|
42
|
+
this.#unregisterAgent?.();
|
|
43
|
+
this.#unregisterAgent = undefined;
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
26
46
|
}
|
|
27
47
|
open() {
|
|
28
48
|
this.#open = true;
|
|
@@ -32,17 +52,11 @@ export class MilkySseEndpoint {
|
|
|
32
52
|
}
|
|
33
53
|
async stop() {
|
|
34
54
|
this.#open = false;
|
|
35
|
-
this.#
|
|
36
|
-
this.#started = false;
|
|
55
|
+
await this.#lifecycle.stop();
|
|
37
56
|
this.#unregisterAgent?.();
|
|
38
57
|
this.#unregisterAgent = undefined;
|
|
39
|
-
if (this.#reconnectTimer) {
|
|
40
|
-
clearTimeout(this.#reconnectTimer);
|
|
41
|
-
this.#reconnectTimer = undefined;
|
|
42
|
-
}
|
|
43
58
|
this.#stream?.close();
|
|
44
59
|
this.#stream = undefined;
|
|
45
|
-
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name, mode: 'sse' }));
|
|
46
60
|
}
|
|
47
61
|
async send({ target, payload }) {
|
|
48
62
|
const message = formatOutboundSegments(payload);
|
|
@@ -147,6 +161,7 @@ export class MilkySseEndpoint {
|
|
|
147
161
|
#admitMessage(data, event) {
|
|
148
162
|
const target = formatInboundTarget(data);
|
|
149
163
|
const content = formatInboundContent(data);
|
|
164
|
+
const segments = formatInboundSegments(data);
|
|
150
165
|
const audioUrl = extractInboundAudioUrl(data);
|
|
151
166
|
const nickname = senderNickname(data);
|
|
152
167
|
const mentioned = isMentioned(data, event.self_id);
|
|
@@ -154,6 +169,7 @@ export class MilkySseEndpoint {
|
|
|
154
169
|
adapter: this.#options.id,
|
|
155
170
|
target,
|
|
156
171
|
content,
|
|
172
|
+
segments,
|
|
157
173
|
sender: String(data.sender_id),
|
|
158
174
|
id: formatInboundMessageId(data),
|
|
159
175
|
metadata: Object.freeze({
|
|
@@ -176,7 +192,7 @@ export class MilkySseEndpoint {
|
|
|
176
192
|
}));
|
|
177
193
|
});
|
|
178
194
|
}
|
|
179
|
-
async #connect() {
|
|
195
|
+
async #connect(handle) {
|
|
180
196
|
const { url, headers, safeUrl } = buildSseConnectOptions(this.#options.config);
|
|
181
197
|
const create = this.#options.createSseStream ?? ((opts) => openSseStream(opts));
|
|
182
198
|
await new Promise((resolve, reject) => {
|
|
@@ -210,20 +226,30 @@ export class MilkySseEndpoint {
|
|
|
210
226
|
},
|
|
211
227
|
});
|
|
212
228
|
this.#stream = stream;
|
|
229
|
+
handle.onForceClose(() => {
|
|
230
|
+
try {
|
|
231
|
+
stream.close();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
/* ignore */
|
|
235
|
+
}
|
|
236
|
+
});
|
|
213
237
|
void stream.closed.then(() => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
238
|
+
// stop-during-connect 竞态由基座静默 settle(主动停止不算失败),此处仅对运行期断开告警
|
|
239
|
+
if (this.#lifecycle.state !== 'stopped') {
|
|
240
|
+
logger.warn(formatCompact({
|
|
241
|
+
op: 'disconnect',
|
|
242
|
+
endpoint: this.#options.config.name,
|
|
243
|
+
mode: 'sse',
|
|
244
|
+
reconnect_ms: this.#options.config.reconnect_interval,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
// 基座语义:仅曾 open 的连接才武装重连;初始连接失败由 start() 的 catch 复位
|
|
248
|
+
handle.notifyClosed(new Error('Milky SSE closed'));
|
|
222
249
|
if (!settled) {
|
|
223
250
|
settled = true;
|
|
224
251
|
reject(new Error('Milky SSE closed before open'));
|
|
225
252
|
}
|
|
226
|
-
this.#scheduleReconnect();
|
|
227
253
|
});
|
|
228
254
|
});
|
|
229
255
|
}
|
|
@@ -241,21 +267,4 @@ export class MilkySseEndpoint {
|
|
|
241
267
|
}));
|
|
242
268
|
}
|
|
243
269
|
}
|
|
244
|
-
#scheduleReconnect() {
|
|
245
|
-
if (this.#stopping || !this.#started || this.#reconnectTimer)
|
|
246
|
-
return;
|
|
247
|
-
const delay = this.#options.config.reconnect_interval;
|
|
248
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
249
|
-
this.#reconnectTimer = undefined;
|
|
250
|
-
void this.#connect().catch((err) => {
|
|
251
|
-
logger.warn(formatCompact({
|
|
252
|
-
op: 'reconnect',
|
|
253
|
-
endpoint: this.#options.config.name,
|
|
254
|
-
mode: 'sse',
|
|
255
|
-
ok: false,
|
|
256
|
-
error: err instanceof Error ? err.message : String(err),
|
|
257
|
-
}));
|
|
258
|
-
});
|
|
259
|
-
}, delay);
|
|
260
|
-
}
|
|
261
270
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
1
|
+
import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
|
|
2
2
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
4
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
@@ -12,6 +12,7 @@ export interface MilkyWebhookEndpointOptions {
|
|
|
12
12
|
}
|
|
13
13
|
export declare class MilkyWebhookEndpoint implements EndpointInstance {
|
|
14
14
|
#private;
|
|
15
|
+
readonly management: EndpointManagement;
|
|
15
16
|
constructor(options: MilkyWebhookEndpointOptions);
|
|
16
17
|
start(): Promise<void>;
|
|
17
18
|
open(): void;
|
package/lib/webhook-endpoint.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
2
|
import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
|
|
3
|
+
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
3
4
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
4
|
-
import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
5
|
+
import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
5
6
|
const logger = getLogger('milky');
|
|
6
7
|
export class MilkyWebhookEndpoint {
|
|
7
8
|
#options;
|
|
8
9
|
#callApi;
|
|
10
|
+
management = createMilkyEndpointManagement(this);
|
|
9
11
|
#routeReleases = [];
|
|
10
12
|
#open = false;
|
|
11
13
|
#started = false;
|
|
@@ -144,6 +146,7 @@ export class MilkyWebhookEndpoint {
|
|
|
144
146
|
#admitMessage(data, event) {
|
|
145
147
|
const target = formatInboundTarget(data);
|
|
146
148
|
const content = formatInboundContent(data);
|
|
149
|
+
const segments = formatInboundSegments(data);
|
|
147
150
|
const audioUrl = extractInboundAudioUrl(data);
|
|
148
151
|
const nickname = senderNickname(data);
|
|
149
152
|
const mentioned = isMentioned(data, event.self_id);
|
|
@@ -151,6 +154,7 @@ export class MilkyWebhookEndpoint {
|
|
|
151
154
|
adapter: this.#options.id,
|
|
152
155
|
target,
|
|
153
156
|
content,
|
|
157
|
+
segments,
|
|
154
158
|
sender: String(data.sender_id),
|
|
155
159
|
id: formatInboundMessageId(data),
|
|
156
160
|
metadata: Object.freeze({
|
package/lib/ws-endpoint.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EndpointInstance, type EndpointManagement } from '@zhin.js/adapter';
|
|
2
2
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
3
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
4
4
|
import { callApi, type MilkyEvent, type MilkyWsConfig } from './protocol.js';
|
|
@@ -12,6 +12,7 @@ export interface MilkyWsEndpointOptions {
|
|
|
12
12
|
}
|
|
13
13
|
export declare class MilkyWsEndpoint implements EndpointInstance {
|
|
14
14
|
#private;
|
|
15
|
+
readonly management: EndpointManagement;
|
|
15
16
|
constructor(options: MilkyWsEndpointOptions);
|
|
16
17
|
start(): Promise<void>;
|
|
17
18
|
open(): void;
|