shipmyagent 1.0.69 → 1.0.72
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/bin/adapters/feishu.js +1 -1
- package/bin/adapters/qq.js +1 -1
- package/bin/adapters/telegram/bot.js +2 -2
- package/bin/agent/context/prompt.d.ts.map +1 -1
- package/bin/agent/context/prompt.js +16 -8
- package/bin/agent/context/prompt.js.map +1 -1
- package/bin/agent/tools/builtin/chat.d.ts.map +1 -1
- package/bin/agent/tools/builtin/chat.js +50 -7
- package/bin/agent/tools/builtin/chat.js.map +1 -1
- package/bin/chat/egress-idempotency.d.ts.map +1 -0
- package/bin/chat/egress-idempotency.js +96 -0
- package/bin/chat/egress-idempotency.js.map +1 -0
- package/package.json +1 -1
package/bin/adapters/feishu.js
CHANGED
|
@@ -38,7 +38,7 @@ export class FeishuBot extends BaseChatAdapter {
|
|
|
38
38
|
this.adminUserIds = new Set((adminUserIds || []).map((x) => String(x)));
|
|
39
39
|
}
|
|
40
40
|
buildChatKey(chatId) {
|
|
41
|
-
return `feishu
|
|
41
|
+
return `feishu-chat-${chatId}`;
|
|
42
42
|
}
|
|
43
43
|
getChatKey(params) {
|
|
44
44
|
return this.buildChatKey(params.chatId);
|
package/bin/adapters/qq.js
CHANGED
|
@@ -61,7 +61,7 @@ export class QQBot extends BaseChatAdapter {
|
|
|
61
61
|
const chatType = typeof params.chatType === "string" && params.chatType
|
|
62
62
|
? params.chatType
|
|
63
63
|
: "unknown";
|
|
64
|
-
return `qq
|
|
64
|
+
return `qq-${chatType}-${params.chatId}`;
|
|
65
65
|
}
|
|
66
66
|
async sendTextToPlatform(params) {
|
|
67
67
|
const chatType = typeof params.chatType === "string" ? params.chatType : "";
|
|
@@ -45,9 +45,9 @@ export class TelegramBot extends BaseChatAdapter {
|
|
|
45
45
|
if (typeof messageThreadId === "number" &&
|
|
46
46
|
Number.isFinite(messageThreadId) &&
|
|
47
47
|
messageThreadId > 0) {
|
|
48
|
-
return `telegram
|
|
48
|
+
return `telegram-chat-${chatId}-topic-${messageThreadId}`;
|
|
49
49
|
}
|
|
50
|
-
return `telegram
|
|
50
|
+
return `telegram-chat-${chatId}`;
|
|
51
51
|
}
|
|
52
52
|
getChatKey(params) {
|
|
53
53
|
return this.buildChatKey(params.chatId, params.messageThreadId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../../src/agent/context/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,IAAI,CAAC;AAExC;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,GAAG,MAAM,CAsBT;AAOD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,MAAM,EAAE,GAChB,kBAAkB,EAAE,CAMtB;AAED,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../../src/agent/context/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,IAAI,CAAC;AAExC;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B,GAAG,MAAM,CAsBT;AAOD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGhE;AAED,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,MAAM,EAAE,GAChB,kBAAkB,EAAE,CAMtB;AAED,eAAO,MAAM,oBAAoB,0iLAkDhC,CAAC"}
|
|
@@ -45,16 +45,18 @@ export function transformPromptsIntoSystemMessages(prompts) {
|
|
|
45
45
|
return result;
|
|
46
46
|
}
|
|
47
47
|
export const DEFAULT_SHIP_PROMPTS = `
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- 遇到失败:给出可复现的错误摘要(1-3 行)+ 下一步行动(重试/降级/需要用户提供信息),不要假装成功。
|
|
51
|
-
|
|
52
|
-
输出与发送(重要)
|
|
48
|
+
# 最重要
|
|
49
|
+
【关于消息】
|
|
53
50
|
- 这是 tool-strict 聊天集成:用户可见内容必须通过 \`chat_send\` 发送。
|
|
54
|
-
-
|
|
51
|
+
- 对所有的用户消息,通过 \`chat_send\`回复, 基于场景决定何时回复:
|
|
52
|
+
- 默认一般一条用户消息回复一次,把完整回复放进同一次 \`chat_send\` 里。
|
|
53
|
+
- 基于不同场景,同一条用户消息可以回复多次,模拟真实对话。
|
|
54
|
+
- 对某些skills或者任务你需要执行时,可以先发送一条回复等等。
|
|
55
|
+
(所有的设计都是为了模拟真实对话逻辑)
|
|
55
56
|
- 不要为了“补充说明/最后一句/再确认”反复调用 \`chat_send\`,避免刷屏。
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
# 很重要
|
|
59
|
+
【上下文加载工具使用规范】(避免乱调用)
|
|
58
60
|
- \`chat_load_history\` / \`agent_load_context\` 都是“补上下文”的工具:只有当你**确实缺少上文信息**、并且**不调用就无法可靠完成任务**时才调用。
|
|
59
61
|
- 若用户问题在当前消息与已知信息内即可回答:不要调用这两个工具(避免浪费 tokens 与污染上下文)。
|
|
60
62
|
- 一次用户请求里,优先只调用**其中一个**;只有在仍然缺关键上下文、且明确知道另一个来源能补齐时,才考虑再调用第二个。
|
|
@@ -84,7 +86,13 @@ Telegram 附件发送(仅当你在 Telegram 对话中回复时)
|
|
|
84
86
|
安全与边界
|
|
85
87
|
- 不要执行破坏性命令(如 \`rm -rf\`、\`git reset --hard\`)除非用户明确要求。
|
|
86
88
|
|
|
89
|
+
User-facing output rules:
|
|
90
|
+
- Reply in natural language.
|
|
91
|
+
- Do NOT paste raw tool outputs or JSON logs; summarize them.
|
|
92
|
+
- Deliver user-visible replies via the \`chat_send\` tool.
|
|
93
|
+
|
|
94
|
+
# 一些补充:
|
|
87
95
|
|
|
88
|
-
{{current_time}}
|
|
96
|
+
当前时间: {{current_time}}
|
|
89
97
|
`;
|
|
90
98
|
//# sourceMappingURL=prompt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../../src/agent/context/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAKxC;IACC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;IAErE,MAAM,mBAAmB,GAAa;QACpC,kBAAkB;QAClB,mBAAmB,WAAW,EAAE;QAChC,cAAc,OAAO,EAAE;QACvB,iBAAiB,SAAS,EAAE;KAC7B,CAAC;IAEF,IAAI,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrE,mBAAmB,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,WAAW,GAAG;QAClB,2BAA2B;QAC3B,8BAA8B;QAC9B,+DAA+D;QAC/D,0DAA0D;KAC3D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,oBAAoB;IAC3B,kCAAkC;IAClC,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAc;IACtD,IAAI,CAAC,MAAM;QAAE,OAAO,MAAM,CAAC;IAC3B,OAAO,MAAM,CAAC,UAAU,CAAC,kBAAkB,EAAE,oBAAoB,EAAE,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,kCAAkC,CAChD,OAAiB;IAEjB,MAAM,MAAM,GAAyB,EAAE,CAAC;IACxC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACvB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../../src/agent/context/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAKxC;IACC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;IAErE,MAAM,mBAAmB,GAAa;QACpC,kBAAkB;QAClB,mBAAmB,WAAW,EAAE;QAChC,cAAc,OAAO,EAAE;QACvB,iBAAiB,SAAS,EAAE;KAC7B,CAAC;IAEF,IAAI,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrE,mBAAmB,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,WAAW,GAAG;QAClB,2BAA2B;QAC3B,8BAA8B;QAC9B,+DAA+D;QAC/D,0DAA0D;KAC3D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,oBAAoB;IAC3B,kCAAkC;IAClC,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAc;IACtD,IAAI,CAAC,MAAM;QAAE,OAAO,MAAM,CAAC;IAC3B,OAAO,MAAM,CAAC,UAAU,CAAC,kBAAkB,EAAE,oBAAoB,EAAE,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,kCAAkC,CAChD,OAAiB;IAEjB,MAAM,MAAM,GAAyB,EAAE,CAAC;IACxC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACvB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDnC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../../src/agent/tools/builtin/chat.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../../../src/agent/tools/builtin/chat.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAiBH,eAAO,MAAM,SAAS;;;;;;;;;EA0FpB,CAAC;AAEH,eAAO,MAAM,SAAS;;;;;;;;;;;CAAgB,CAAC"}
|
|
@@ -9,6 +9,8 @@ import { z } from "zod";
|
|
|
9
9
|
import { tool } from "ai";
|
|
10
10
|
import { chatRequestContext } from "../../../chat/request-context.js";
|
|
11
11
|
import { getChatDispatcher } from "../../../chat/dispatcher.js";
|
|
12
|
+
import { getToolRuntimeContext } from "../set/runtime-context.js";
|
|
13
|
+
import { markChatEgressChatSendDelivered, releaseChatEgressChatSendClaim, tryClaimChatEgressChatSend, } from "../../../chat/egress-idempotency.js";
|
|
12
14
|
const chatSendInputSchema = z.object({
|
|
13
15
|
text: z.string().describe("Text to send back to the current chat."),
|
|
14
16
|
});
|
|
@@ -47,13 +49,54 @@ export const chat_send = tool({
|
|
|
47
49
|
error: `No dispatcher registered for channel: ${channel}`,
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
const text = String(input.text ?? "");
|
|
53
|
+
// 关键防线:同一条 inbound messageId 只允许发送一次,避免模型在 tool-loop 中重复 chat_send 造成刷屏。
|
|
54
|
+
let markerFile = undefined;
|
|
55
|
+
if (typeof messageId === "string" && messageId.trim()) {
|
|
56
|
+
try {
|
|
57
|
+
const { projectRoot } = getToolRuntimeContext();
|
|
58
|
+
const claim = await tryClaimChatEgressChatSend({
|
|
59
|
+
projectRoot,
|
|
60
|
+
channel,
|
|
61
|
+
chatId,
|
|
62
|
+
messageId,
|
|
63
|
+
meta: { textPreview: text.slice(0, 200) },
|
|
64
|
+
});
|
|
65
|
+
if (!claim.claimed) {
|
|
66
|
+
return { success: true, skipped: true, reason: claim.reason };
|
|
67
|
+
}
|
|
68
|
+
markerFile = claim.markerFile || undefined;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// ignore (best-effort)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const r = await dispatcher.sendText({
|
|
76
|
+
chatId,
|
|
77
|
+
text,
|
|
78
|
+
...(typeof messageThreadId === "number" ? { messageThreadId } : {}),
|
|
79
|
+
...(typeof chatType === "string" && chatType ? { chatType } : {}),
|
|
80
|
+
...(typeof messageId === "string" && messageId ? { messageId } : {}),
|
|
81
|
+
});
|
|
82
|
+
if (r?.success && markerFile) {
|
|
83
|
+
await markChatEgressChatSendDelivered({
|
|
84
|
+
markerFile,
|
|
85
|
+
deliveredMeta: { textLen: text.length },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!r?.success && markerFile) {
|
|
89
|
+
// 发送失败:释放 claim,允许后续重试(最佳努力)。
|
|
90
|
+
await releaseChatEgressChatSendClaim(markerFile);
|
|
91
|
+
}
|
|
92
|
+
return r;
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
if (markerFile) {
|
|
96
|
+
await releaseChatEgressChatSendClaim(markerFile);
|
|
97
|
+
}
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
57
100
|
},
|
|
58
101
|
});
|
|
59
102
|
export const chatTools = { chat_send };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../../../../src/agent/tools/builtin/chat.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAA4B,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../../../../src/agent/tools/builtin/chat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress-idempotency.d.ts","sourceRoot":"","sources":["../../src/chat/egress-idempotency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,GAAG,OAAO,CACP;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CACrC,CAkDA;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB;AAED,wBAAsB,8BAA8B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat egress idempotency (best-effort de-duplication for outbound sends).
|
|
3
|
+
*
|
|
4
|
+
* 问题背景
|
|
5
|
+
* - 在 tool-strict 模式下,模型需要通过 `chat_send` 才能把消息发回平台。
|
|
6
|
+
* - 现实里模型偶尔会在 tool-loop 里重复调用 `chat_send`(甚至参数完全相同),
|
|
7
|
+
* 导致同一条用户消息触发多条重复回复。
|
|
8
|
+
*
|
|
9
|
+
* 设计目标
|
|
10
|
+
* - 以「同一条 inbound messageId」为维度做幂等:同一条用户消息最多发送一次(默认策略)。
|
|
11
|
+
* - 采用本地文件原子创建(flag: 'wx')实现跨进程去重,避免多实例或重启造成重复发送。
|
|
12
|
+
*
|
|
13
|
+
* 存储布局
|
|
14
|
+
* - `${projectRoot}/.ship/.cache/egress/chat_send/<channel>/<encode(chatId)>/<encode(messageId)>.json`
|
|
15
|
+
*
|
|
16
|
+
* 注意
|
|
17
|
+
* - 只有在能拿到稳定的 `messageId` 时才启用去重;缺失时不阻断发送。
|
|
18
|
+
* - I/O 全部 best-effort:任何异常都不应阻断正常回复。
|
|
19
|
+
*/
|
|
20
|
+
import path from "path";
|
|
21
|
+
import fs from "fs-extra";
|
|
22
|
+
import { getCacheDirPath } from "../utils.js";
|
|
23
|
+
export async function tryClaimChatEgressChatSend(params) {
|
|
24
|
+
const projectRoot = String(params.projectRoot || "").trim();
|
|
25
|
+
const channel = params.channel;
|
|
26
|
+
const chatId = String(params.chatId || "").trim();
|
|
27
|
+
const messageId = String(params.messageId || "").trim();
|
|
28
|
+
const meta = params.meta;
|
|
29
|
+
if (!projectRoot || !channel || !chatId || !messageId) {
|
|
30
|
+
return { claimed: false, reason: "missing_key_fields" };
|
|
31
|
+
}
|
|
32
|
+
const dir = path.join(getCacheDirPath(projectRoot), "egress", "chat_send", channel, encodeURIComponent(chatId));
|
|
33
|
+
const markerFile = path.join(dir, `${encodeURIComponent(messageId)}.json`);
|
|
34
|
+
try {
|
|
35
|
+
await fs.ensureDir(dir);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// 无法创建目录时,不做去重,直接允许发送(避免掉消息)。
|
|
39
|
+
return { claimed: true };
|
|
40
|
+
}
|
|
41
|
+
const payload = {
|
|
42
|
+
v: 1,
|
|
43
|
+
channel,
|
|
44
|
+
chatId,
|
|
45
|
+
messageId,
|
|
46
|
+
status: "inflight",
|
|
47
|
+
claimedAt: Date.now(),
|
|
48
|
+
...(meta && typeof meta === "object" ? { meta } : {}),
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
await fs.writeFile(markerFile, JSON.stringify(payload, null, 2), {
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
flag: "wx",
|
|
54
|
+
});
|
|
55
|
+
return { claimed: true, markerFile };
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (e && typeof e === "object" && e.code === "EEXIST") {
|
|
59
|
+
return { claimed: false, reason: "already_claimed" };
|
|
60
|
+
}
|
|
61
|
+
// 未知错误:不要阻断发送(宁可重复,也不要丢消息)。
|
|
62
|
+
return { claimed: true };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function markChatEgressChatSendDelivered(params) {
|
|
66
|
+
const markerFile = String(params.markerFile || "").trim();
|
|
67
|
+
if (!markerFile)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
const existing = (await fs.readJson(markerFile).catch(() => null));
|
|
71
|
+
const next = {
|
|
72
|
+
...(existing && typeof existing === "object" ? existing : {}),
|
|
73
|
+
status: "delivered",
|
|
74
|
+
deliveredAt: Date.now(),
|
|
75
|
+
...(params.deliveredMeta && typeof params.deliveredMeta === "object"
|
|
76
|
+
? { deliveredMeta: params.deliveredMeta }
|
|
77
|
+
: {}),
|
|
78
|
+
};
|
|
79
|
+
await fs.writeJson(markerFile, next, { spaces: 2 });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function releaseChatEgressChatSendClaim(markerFile) {
|
|
86
|
+
const file = String(markerFile || "").trim();
|
|
87
|
+
if (!file)
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
await fs.remove(file);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=egress-idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress-idempotency.js","sourceRoot":"","sources":["../../src/chat/egress-idempotency.ts"],"names":[],"mappings":"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"}
|