docs/plans/feishu-observed-contact-label-enrichment.md
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Resolve Feishu sender and group IDs into recognizable observed-contact labels without delaying inbound message processing.
Architecture: ChannelBase exposes a synchronous post-observation hook and its existing protected persistence path. FeishuChannel starts cached, fire-and-forget OpenAPI lookups from that hook, then writes one enriched observation after successful resolution. The existing store schema and non-Feishu channels remain unchanged.
Tech Stack: TypeScript, Node.js 22+, Vitest, Feishu tenant-access-token OpenAPI, existing channel-base observed-contact sink.
handleInbound or the Agent prompt path.FeishuChannel instance; concurrent observations share the same promise.contact:user.basic_profile:readonly and im:chat:readonly; do not use full-contact APIs.any, and existing file naming.Files:
packages/channels/base/src/ChannelBase.tspackages/channels/base/src/ChannelBase.test.tsInterfaces:
Produces: protected onObservedContact(envelope: Envelope): void
Produces: protected recordObservedContact(envelope: Envelope): Promise<void>
Guarantees: the hook runs once, after the initial persistence attempt, only for preflight-approved envelopes.
Step 1: Write the failing hook-order test
Add a local test subclass and assert that persistence happens before the hook:
class ObservedHookChannel extends TestChannel {
readonly observedEnvelopes: Envelope[] = [];
protected override onObservedContact(envelope: Envelope): void {
this.observedEnvelopes.push(envelope);
}
}
it('notifies the adapter after an approved contact is persisted', async () => {
const order: string[] = [];
const observe = vi.fn(() => {
order.push('persisted');
});
const ch = new ObservedHookChannel('test-chan', defaultConfig(), bridge, {
observedContacts: { observe },
});
const message = envelope();
await ch.handleInbound(message);
expect(order).toEqual(['persisted']);
expect(ch.observedEnvelopes).toEqual([message]);
expect(bridge.prompt).toHaveBeenCalled();
});
Run:
cd packages/channels/base
npx vitest run src/ChannelBase.test.ts -t "notifies the adapter after an approved contact is persisted"
Expected: compilation fails because onObservedContact does not exist.
Change only the persistence method visibility from private to protected; keep its body unchanged. Add the default no-op hook and invoke it synchronously after the initial observation:
protected async recordObservedContact(envelope: Envelope): Promise<void> {
protected onObservedContact(_envelope: Envelope): void {}
// Inside processInbound, after await this.recordObservedContact(envelope):
this.onObservedContact(envelope);
Run:
cd packages/channels/base
npx vitest run src/ChannelBase.test.ts
Expected: all ChannelBase tests pass.
Files:
packages/channels/feishu/src/FeishuAdapter.tspackages/channels/feishu/src/adapter.test.tsInterfaces:
Consumes: onObservedContact(envelope) and recordObservedContact(envelope) from Task 1.
Produces: process-local user/group name and lookup-promise caches.
Produces: basic_batch requests with the callback ID type and GET /im/v1/chats/:chat_id requests.
Step 1: Write failing success and cache tests
Allow the test factory to pass ChannelBaseOptions, preload tokenCache, and send two complete Feishu group-event fixtures with different message IDs. Mock these complete API responses:
new Response(
JSON.stringify({
code: 0,
msg: 'success',
data: { users: [{ user_id: 'ou_user', name: 'Alice' }] },
}),
{ status: 200 },
);
new Response(
JSON.stringify({
code: 0,
msg: 'success',
data: { name: 'Project Group' },
}),
{ status: 200 },
);
Assert the observable store calls, not private maps:
expect(observe).toHaveBeenNthCalledWith(1, 'test', {
user: { id: 'ou_user', label: 'ou_user' },
group: { id: 'oc_group', label: 'oc_group' },
});
expect(observe).toHaveBeenNthCalledWith(2, 'test', {
user: { id: 'ou_user', label: 'Alice' },
group: { id: 'oc_group', label: 'Project Group' },
});
After the first lookup settles, deliver the second fixture and assert its first observation already contains the cached names and that each OpenAPI endpoint was requested exactly once.
Use an externally controlled rejected lookup promise, a real mock bridge, and a no-op onPromptStart test subclass. Assert bridge.prompt runs while lookup work is unresolved. After rejecting the lookups, assert the ID observation remains and process.stderr.write received no lookup error.
Run:
cd packages/channels/feishu
npx vitest run src/adapter.test.ts -t "observed contact"
Expected: the enriched observation and background request assertions fail because Feishu does not yet resolve labels.
Add successful-name maps and promise caches:
private readonly observedUserNames = new Map<string, string>();
private readonly observedChatNames = new Map<string, string>();
private readonly observedUserLookups = new Map<
string,
Promise<string | undefined>
>();
private readonly observedChatLookups = new Map<
string,
Promise<string | undefined>
>();
Populate later envelopes synchronously from successful caches:
const cachedSenderName = this.observedUserNames.get(senderId);
const cachedChatName = isGroup ? this.observedChatNames.get(chatId) : undefined;
const envelope: Envelope = {
channelName: this.name,
senderId,
senderName: cachedSenderName || senderId,
chatId,
text: cleanText,
messageId: msgId,
threadId: msg.root_id || undefined,
isGroup,
isMentioned,
isReplyToBot: false,
...(cachedChatName ? { chatName: cachedChatName } : {}),
};
Override the base hook without returning or awaiting the lookup promise:
protected override onObservedContact(envelope: Envelope): void {
void this.enrichObservedContact(envelope);
}
Implement user and chat request helpers with AbortSignal.timeout(15_000). Convert ou_ to open_id, on_ to union_id, and other IDs to user_id. Return undefined for missing tokens, non-2xx responses, non-zero API codes, malformed JSON, empty names, and thrown errors without writing logs. Store the promise before awaiting it so concurrent messages share the same request; retain resolved undefined promises to suppress retries until restart.
After Promise.all, call:
await this.recordObservedContact({
...envelope,
...(senderName ? { senderName } : {}),
...(chatName ? { chatName } : {}),
});
only when at least one lookup returned a name.
Run:
cd packages/channels/feishu
npx vitest run src/adapter.test.ts
Expected: all Feishu adapter tests pass with no unexpected output from the new failure cases.
Files:
packages/channels/base/src/ChannelBase.tspackages/channels/base/src/ChannelBase.test.tspackages/channels/feishu/src/FeishuAdapter.tspackages/channels/feishu/src/adapter.test.tsdocs/design/feishu-observed-contact-label-enrichment.mddocs/plans/feishu-observed-contact-label-enrichment.mdInterfaces:
Links: GitHub issue QwenLM/qwen-code#8566.
Produces: one draft pull request from BenGuanRan:feat/feishu-observed-contact-labels to QwenLM/qwen-code:main.
Step 1: Run focused verification
cd packages/channels/base && npx vitest run src/ChannelBase.test.ts
cd ../feishu && npx vitest run src/adapter.test.ts
cd ../../.. && npm run build && npm run typecheck
git diff --check origin/main...HEAD
Read git diff origin/main...HEAD in open-ended passes. Check every changed behavior against the design, verify the tests would fail if the hook or enrichment write were removed, and stop after two consecutive clean passes.
Review the exact branch diff against origin/main. Triage every finding as valid, false positive, or overthinking; accepted fixes return to focused tests and self-audit.
git add -- packages/channels/base/src/ChannelBase.ts packages/channels/base/src/ChannelBase.test.ts packages/channels/feishu/src/FeishuAdapter.ts packages/channels/feishu/src/adapter.test.ts docs/plans/feishu-observed-contact-label-enrichment.md
git commit -m "feat(feishu): enrich observed contact labels"
git push -u fork feat/feishu-observed-contact-labels
Create exactly one draft PR with title feat(feishu): enrich observed contact labels, base QwenLM/qwen-code:main, head BenGuanRan:feat/feishu-observed-contact-labels, and Closes #8566. Fill every English template section and provide a complete paragraph-for-paragraph Chinese translation in the <details> block.