Skip to content

Commit 95aced6

Browse files
committed
feat: refine presentation workflow and outbound delivery
1 parent b2066dc commit 95aced6

27 files changed

Lines changed: 1290 additions & 164 deletions

scripts/im/weixin_sidecar.mjs

Lines changed: 189 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fs from "node:fs";
44
import path from "node:path";
55
import os from "node:os";
66
import readline from "node:readline";
7-
import { pathToFileURL } from "node:url";
7+
import { fileURLToPath, pathToFileURL } from "node:url";
88

99
function emit(event, payload = {}) {
1010
process.stdout.write(`${JSON.stringify({ type: "event", event, ...payload })}\n`);
@@ -49,7 +49,7 @@ function writeState(stateFile, patch) {
4949
}
5050

5151
async function loadWeixinInternalModule(packageEntryUrl, relCandidates) {
52-
const entryPath = new URL(packageEntryUrl).pathname;
52+
const entryPath = fileURLToPath(packageEntryUrl);
5353
const packageRoot = path.resolve(path.dirname(entryPath), "..");
5454
for (const rel of relCandidates) {
5555
const abs = path.resolve(packageRoot, rel);
@@ -66,13 +66,30 @@ async function loadWeixinInternalModule(packageEntryUrl, relCandidates) {
6666
async function loadWeixinBundledInternals(packageEntryUrl) {
6767
if (!packageEntryUrl) return null;
6868
try {
69-
const entryPath = new URL(packageEntryUrl).pathname;
69+
const entryPath = fileURLToPath(packageEntryUrl);
7070
const source = fs.readFileSync(entryPath, "utf8");
7171
const tempPath = path.join(
7272
os.tmpdir(),
7373
`cccc-weixin-sdk-internals-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`,
7474
);
75-
const extra = "\nexport { startWeixinLoginWithQr, waitForWeixinLogin, normalizeAccountId, saveWeixinAccount, registerWeixinAccountId };\nexport const DEFAULT_ILINK_BOT_TYPE = '3';\n";
75+
const extra = `
76+
export {
77+
startWeixinLoginWithQr,
78+
waitForWeixinLogin,
79+
normalizeAccountId,
80+
saveWeixinAccount,
81+
registerWeixinAccountId,
82+
resolveWeixinAccount,
83+
listWeixinAccountIds,
84+
markdownToPlainText,
85+
sendMessageWeixin,
86+
sendWeixinMediaFile
87+
};
88+
export const DEFAULT_ILINK_BOT_TYPE = '3';
89+
export function __ccccGetContextToken(accountId, userId) {
90+
return contextTokenStore.get(contextTokenKey(accountId, userId)) || "";
91+
}
92+
`;
7693
fs.writeFileSync(tempPath, source + extra, "utf8");
7794
return await import(pathToFileURL(tempPath).href);
7895
} catch {
@@ -84,9 +101,11 @@ async function main() {
84101
const args = parseArgs(process.argv);
85102
let sdk;
86103
let sdkEntryUrl = "";
104+
let bundledInternals = null;
87105
try {
88106
sdkEntryUrl = await import.meta.resolve("weixin-agent-sdk");
89107
sdk = await import("weixin-agent-sdk");
108+
bundledInternals = await loadWeixinBundledInternals(sdkEntryUrl);
90109
} catch (error) {
91110
writeState(args.stateFile, {
92111
status: "error",
@@ -100,7 +119,8 @@ async function main() {
100119
process.exit(1);
101120
}
102121

103-
const { isLoggedIn, start, login, logout } = sdk;
122+
const runtimeSdk = bundledInternals || sdk;
123+
const { isLoggedIn, start, login, logout } = runtimeSdk;
104124
if (typeof isLoggedIn !== "function") {
105125
writeState(args.stateFile, {
106126
status: "error",
@@ -162,12 +182,12 @@ async function main() {
162182
"dist/auth/accounts.js",
163183
])
164184
: null;
165-
const bundledInternals =
166-
!loginQrModule || !accountsModule ? await loadWeixinBundledInternals(sdkEntryUrl) : null;
185+
const loginBundledInternals =
186+
!loginQrModule || !accountsModule ? bundledInternals : null;
167187

168188
let accountId = "";
169-
const loginInternals = loginQrModule || bundledInternals;
170-
const accountInternals = accountsModule || bundledInternals;
189+
const loginInternals = loginQrModule || loginBundledInternals;
190+
const accountInternals = accountsModule || loginBundledInternals;
171191
if (loginInternals && accountInternals) {
172192
const startWeixinLoginWithQr = loginInternals.startWeixinLoginWithQr;
173193
const waitForWeixinLogin = loginInternals.waitForWeixinLogin;
@@ -313,14 +333,169 @@ async function main() {
313333
const accountId = String(process.env.CCCC_IM_WEIXIN_ACCOUNT_ID || "").trim() || undefined;
314334
const pendingReplies = new Map();
315335
const abortController = new AbortController();
336+
const sendMessageWeixin = typeof runtimeSdk.sendMessageWeixin === "function"
337+
? runtimeSdk.sendMessageWeixin
338+
: null;
339+
const sendWeixinMediaFile = typeof runtimeSdk.sendWeixinMediaFile === "function"
340+
? runtimeSdk.sendWeixinMediaFile
341+
: null;
342+
const resolveWeixinAccount = typeof runtimeSdk.resolveWeixinAccount === "function"
343+
? runtimeSdk.resolveWeixinAccount
344+
: null;
345+
const listWeixinAccountIds = typeof runtimeSdk.listWeixinAccountIds === "function"
346+
? runtimeSdk.listWeixinAccountIds
347+
: null;
348+
const markdownToPlainText = typeof runtimeSdk.markdownToPlainText === "function"
349+
? runtimeSdk.markdownToPlainText
350+
: (text) => String(text || "");
351+
const getContextToken = typeof runtimeSdk.__ccccGetContextToken === "function"
352+
? runtimeSdk.__ccccGetContextToken
353+
: null;
354+
const runtimeAccountId = (() => {
355+
if (accountId) return accountId;
356+
if (!listWeixinAccountIds) return "";
357+
const ids = listWeixinAccountIds();
358+
if (!Array.isArray(ids) || ids.length <= 0) return "";
359+
if (ids.length > 1) emitLog(`[weixin] detected multiple accounts, using the first: ${ids[0]}`);
360+
return String(ids[0] || "");
361+
})();
362+
const outboundAccount = (() => {
363+
if (!resolveWeixinAccount || !runtimeAccountId) return null;
364+
try {
365+
return resolveWeixinAccount(runtimeAccountId);
366+
} catch (error) {
367+
emitLog(
368+
`[weixin] failed to resolve outbound account ${runtimeAccountId}: ${
369+
error instanceof Error ? error.message : String(error)
370+
}`,
371+
);
372+
return null;
373+
}
374+
})();
316375
writeState(args.stateFile, {
317376
status: "running",
318377
logged_in: true,
319-
account_id: accountId || "",
378+
account_id: runtimeAccountId || "",
320379
qrcode_url: "",
321380
error: "",
322381
});
323382

383+
async function sendDirectText(chatId, text) {
384+
if (!sendMessageWeixin || !getContextToken || !outboundAccount?.token) {
385+
emitLog("[weixin] proactive text send is unavailable");
386+
return;
387+
}
388+
const contextToken = String(getContextToken(outboundAccount.accountId, chatId) || "").trim();
389+
if (!contextToken) {
390+
emitLog(`[weixin] missing outbound context token for chat_id=${chatId}`);
391+
return;
392+
}
393+
await sendMessageWeixin({
394+
to: chatId,
395+
text: markdownToPlainText(String(text || "")),
396+
opts: {
397+
baseUrl: outboundAccount.baseUrl,
398+
token: outboundAccount.token,
399+
contextToken,
400+
},
401+
});
402+
}
403+
404+
async function sendDirectFile(chatId, filePath, caption) {
405+
if (!sendWeixinMediaFile || !getContextToken || !outboundAccount?.token) {
406+
emitLog("[weixin] proactive file send is unavailable");
407+
return;
408+
}
409+
const contextToken = String(getContextToken(outboundAccount.accountId, chatId) || "").trim();
410+
if (!contextToken) {
411+
emitLog(`[weixin] missing outbound context token for chat_id=${chatId}`);
412+
return;
413+
}
414+
await sendWeixinMediaFile({
415+
filePath,
416+
to: chatId,
417+
text: markdownToPlainText(String(caption || "")),
418+
opts: {
419+
baseUrl: outboundAccount.baseUrl,
420+
token: outboundAccount.token,
421+
contextToken,
422+
},
423+
cdnBaseUrl: outboundAccount.cdnBaseUrl,
424+
});
425+
}
426+
427+
async function handleCommand(payload) {
428+
const cmd = String(payload?.cmd || "").trim();
429+
if (cmd === "shutdown") {
430+
abortController.abort();
431+
return;
432+
}
433+
434+
if (cmd === "reply") {
435+
const requestId = String(payload.request_id || "").trim();
436+
if (!requestId) return;
437+
const pending = pendingReplies.get(requestId);
438+
if (!pending) {
439+
emitLog(`missing pending reply handle for request_id=${requestId}`);
440+
return;
441+
}
442+
pendingReplies.delete(requestId);
443+
pending.resolve({
444+
text: String(payload.text || ""),
445+
});
446+
return;
447+
}
448+
449+
if (cmd === "reply_file") {
450+
const requestId = String(payload.request_id || "").trim();
451+
const filePath = String(payload.file_path || "").trim();
452+
if (!requestId || !filePath) return;
453+
const pending = pendingReplies.get(requestId);
454+
if (!pending) {
455+
emitLog(`missing pending reply handle for request_id=${requestId}`);
456+
return;
457+
}
458+
pendingReplies.delete(requestId);
459+
pending.resolve({
460+
text: String(payload.caption || ""),
461+
media: {
462+
url: path.resolve(filePath),
463+
},
464+
});
465+
return;
466+
}
467+
468+
if (cmd === "send") {
469+
const chatId = String(payload.chat_id || "").trim();
470+
if (!chatId) return;
471+
try {
472+
await sendDirectText(chatId, String(payload.text || ""));
473+
} catch (error) {
474+
emitLog(
475+
`[weixin] proactive send failed for ${chatId}: ${
476+
error instanceof Error ? error.message : String(error)
477+
}`,
478+
);
479+
}
480+
return;
481+
}
482+
483+
if (cmd === "send_file") {
484+
const chatId = String(payload.chat_id || "").trim();
485+
const filePath = String(payload.file_path || "").trim();
486+
if (!chatId || !filePath) return;
487+
try {
488+
await sendDirectFile(chatId, path.resolve(filePath), String(payload.caption || ""));
489+
} catch (error) {
490+
emitLog(
491+
`[weixin] proactive file send failed for ${chatId}: ${
492+
error instanceof Error ? error.message : String(error)
493+
}`,
494+
);
495+
}
496+
}
497+
}
498+
324499
const rl = readline.createInterface({
325500
input: process.stdin,
326501
crlfDelay: Infinity,
@@ -337,24 +512,7 @@ async function main() {
337512
return;
338513
}
339514
if (!payload || payload.type !== "cmd") return;
340-
341-
if (payload.cmd === "shutdown") {
342-
abortController.abort();
343-
return;
344-
}
345-
346-
if (payload.cmd !== "reply") return;
347-
const requestId = String(payload.request_id || "").trim();
348-
if (!requestId) return;
349-
const pending = pendingReplies.get(requestId);
350-
if (!pending) {
351-
emitLog(`missing pending reply handle for request_id=${requestId}`);
352-
return;
353-
}
354-
pendingReplies.delete(requestId);
355-
pending.resolve({
356-
text: String(payload.text || ""),
357-
});
515+
void handleCommand(payload);
358516
});
359517

360518
process.on("SIGINT", () => abortController.abort());
@@ -413,19 +571,19 @@ async function main() {
413571
},
414572
};
415573

416-
emit("ready", { account_id: accountId || "" });
574+
emit("ready", { account_id: runtimeAccountId || "" });
417575

418576
try {
419577
await start(agent, {
420-
accountId,
578+
accountId: runtimeAccountId || undefined,
421579
abortSignal: abortController.signal,
422580
log: emitLog,
423581
});
424582
} catch (error) {
425583
writeState(args.stateFile, {
426584
status: "error",
427585
logged_in: isLoggedIn(),
428-
account_id: accountId || "",
586+
account_id: runtimeAccountId || "",
429587
qrcode_url: "",
430588
error: error instanceof Error ? error.message : String(error),
431589
});

src/cccc/kernel/prompt_files.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717

1818
DEFAULT_PREAMBLE_BODY = """Startup routes:
1919
- Cold start or resume: run `cccc_bootstrap`, then `cccc_help`.
20-
- From bootstrap, inspect `context_hygiene`, `memory_recall_gate`, and inbox preview before planning.
21-
- Need colder group or project detail: use `cccc_context_get` / `cccc_project_info`.
20+
- From bootstrap, inspect `context_hygiene`, `memory_recall_gate`, and inbox before planning.
21+
- Need colder group/project detail: use `cccc_context_get` / `cccc_project_info`.
2222
2323
Working stance:
24-
- Work like a sharp teammate, not a script.
24+
- Work like a teammate, not a script.
2525
- Reuse working paths first.
2626
- Prefer silence over low-signal chatter; speak for real changes, not filler or routine `@all` updates.
27-
- For chat, be natural, brief, and direct.
27+
- For chat, be brief and direct; intent is not progress.
2828
- Once scope is approved, finish it end-to-end; do not ask to continue on obvious next steps.
2929
"""
3030

src/cccc/kernel/system_prompt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def render_role_system_prompt(
139139
"- No fabrication. Verify before claiming done.",
140140
"- Visible replies must go through MCP: cccc_message_send / cccc_message_reply.",
141141
"- Terminal output is not delivery.",
142+
"- A status message, plan, or promise is not task progress; for action requests, either start the work now or state the exact blocker.",
142143
"- Cold start or resume: call cccc_bootstrap first, then cccc_help.",
143144
"- At key transitions, sync shared control-plane state and your cccc_agent_state.",
144145
"- Once scope is approved, finish it end-to-end; do not ask to continue on obvious next steps.",

0 commit comments

Comments
 (0)