@zhin.js/adapter-qq 5.0.2 → 6.0.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +75 -409
  3. package/adapters/qq.ts +51 -0
  4. package/agent/tools/add_role.ts +2 -2
  5. package/agent/tools/channel_info.ts +2 -2
  6. package/agent/tools/create_role.ts +2 -2
  7. package/agent/tools/list_channels.ts +2 -2
  8. package/agent/tools/list_guilds.ts +2 -2
  9. package/agent/tools/list_roles.ts +2 -2
  10. package/agent/tools/member_detail.ts +2 -2
  11. package/agent/tools/remove_role.ts +2 -2
  12. package/commands/endpoint/add/[name:string=].ts +21 -0
  13. package/commands/endpoint/cancel.ts +15 -0
  14. package/commands/endpoint/list.ts +10 -0
  15. package/commands/endpoint/remove/[name:string].ts +15 -0
  16. package/lib/endpoint.d.ts +75 -0
  17. package/lib/endpoint.js +358 -0
  18. package/lib/index.d.ts +6 -0
  19. package/lib/index.js +6 -0
  20. package/lib/platform-permit.d.ts +16 -0
  21. package/lib/{src/platform-permit.js → platform-permit.js} +1 -2
  22. package/lib/protocol.d.ts +96 -0
  23. package/lib/protocol.js +129 -0
  24. package/lib/qq-agent-deps.d.ts +21 -0
  25. package/lib/qq-agent-deps.js +30 -0
  26. package/lib/qq-bind-api.d.ts +36 -0
  27. package/lib/{src/qq-bind-api.js → qq-bind-api.js} +0 -1
  28. package/lib/qq-bind-flow.d.ts +19 -0
  29. package/lib/{src/qq-bind-flow.js → qq-bind-flow.js} +0 -1
  30. package/lib/qq-bind-persist.d.ts +12 -0
  31. package/lib/{src/qq-bind-persist.js → qq-bind-persist.js} +4 -5
  32. package/lib/qq-endpoint-commands.d.ts +25 -0
  33. package/lib/qq-endpoint-commands.js +147 -0
  34. package/lib/qq-endpoint-config.d.ts +17 -0
  35. package/lib/qq-endpoint-config.js +110 -0
  36. package/lib/qq-runtime-state.d.ts +17 -0
  37. package/lib/qq-runtime-state.js +13 -0
  38. package/lib/webhook.d.ts +18 -0
  39. package/lib/webhook.js +100 -0
  40. package/lib/ws.d.ts +24 -0
  41. package/lib/ws.js +118 -0
  42. package/package.json +51 -40
  43. package/plugin.ts +16 -0
  44. package/schema.json +85 -0
  45. package/src/endpoint.ts +362 -596
  46. package/src/index.ts +56 -186
  47. package/src/platform-permit.ts +1 -1
  48. package/src/protocol.ts +229 -0
  49. package/src/qq-agent-deps.ts +49 -12
  50. package/src/qq-bind-persist.ts +4 -4
  51. package/src/qq-endpoint-commands.ts +174 -0
  52. package/src/qq-endpoint-config.ts +135 -0
  53. package/src/qq-runtime-state.ts +36 -0
  54. package/src/webhook.ts +127 -0
  55. package/src/ws.ts +172 -0
  56. package/client/Dashboard.tsx +0 -230
  57. package/client/index.tsx +0 -11
  58. package/client/tsconfig.json +0 -7
  59. package/client/utils/api.ts +0 -30
  60. package/dist/index.js +0 -30
  61. package/lib/agent/tools/add_role.js +0 -23
  62. package/lib/agent/tools/add_role.js.map +0 -1
  63. package/lib/agent/tools/channel_info.js +0 -19
  64. package/lib/agent/tools/channel_info.js.map +0 -1
  65. package/lib/agent/tools/create_role.js +0 -22
  66. package/lib/agent/tools/create_role.js.map +0 -1
  67. package/lib/agent/tools/list_channels.js +0 -20
  68. package/lib/agent/tools/list_channels.js.map +0 -1
  69. package/lib/agent/tools/list_guilds.js +0 -17
  70. package/lib/agent/tools/list_guilds.js.map +0 -1
  71. package/lib/agent/tools/list_roles.js +0 -20
  72. package/lib/agent/tools/list_roles.js.map +0 -1
  73. package/lib/agent/tools/member_detail.js +0 -20
  74. package/lib/agent/tools/member_detail.js.map +0 -1
  75. package/lib/agent/tools/remove_role.js +0 -23
  76. package/lib/agent/tools/remove_role.js.map +0 -1
  77. package/lib/package.json +0 -105
  78. package/lib/src/adapter.js +0 -81
  79. package/lib/src/adapter.js.map +0 -1
  80. package/lib/src/endpoint.js +0 -600
  81. package/lib/src/endpoint.js.map +0 -1
  82. package/lib/src/gateway-config.js +0 -32
  83. package/lib/src/gateway-config.js.map +0 -1
  84. package/lib/src/group-at-normalize.js +0 -68
  85. package/lib/src/group-at-normalize.js.map +0 -1
  86. package/lib/src/inbound-action.js +0 -57
  87. package/lib/src/inbound-action.js.map +0 -1
  88. package/lib/src/inbound-normalize.js +0 -47
  89. package/lib/src/inbound-normalize.js.map +0 -1
  90. package/lib/src/index.js +0 -194
  91. package/lib/src/index.js.map +0 -1
  92. package/lib/src/mention-enrich.js +0 -52
  93. package/lib/src/mention-enrich.js.map +0 -1
  94. package/lib/src/outbound-keyboard.js +0 -100
  95. package/lib/src/outbound-keyboard.js.map +0 -1
  96. package/lib/src/outbound-markdown.js +0 -189
  97. package/lib/src/outbound-markdown.js.map +0 -1
  98. package/lib/src/outbound-media.js +0 -51
  99. package/lib/src/outbound-media.js.map +0 -1
  100. package/lib/src/outbound-mixed-media.js +0 -78
  101. package/lib/src/outbound-mixed-media.js.map +0 -1
  102. package/lib/src/platform-permit.js.map +0 -1
  103. package/lib/src/qq-agent-deps.js +0 -10
  104. package/lib/src/qq-agent-deps.js.map +0 -1
  105. package/lib/src/qq-bind-api.js.map +0 -1
  106. package/lib/src/qq-bind-flow.js.map +0 -1
  107. package/lib/src/qq-bind-persist.js.map +0 -1
  108. package/lib/src/qq-endpoint-manager.js +0 -159
  109. package/lib/src/qq-endpoint-manager.js.map +0 -1
  110. package/lib/src/sdk-version.js +0 -19
  111. package/lib/src/sdk-version.js.map +0 -1
  112. package/lib/src/segment-mapper.js +0 -2
  113. package/lib/src/segment-mapper.js.map +0 -1
  114. package/lib/src/types.js +0 -2
  115. package/lib/src/types.js.map +0 -1
  116. package/plugin.yml +0 -3
  117. package/src/adapter.ts +0 -97
  118. package/src/gateway-config.ts +0 -48
  119. package/src/group-at-normalize.ts +0 -75
  120. package/src/inbound-action.ts +0 -87
  121. package/src/inbound-normalize.ts +0 -57
  122. package/src/mention-enrich.ts +0 -71
  123. package/src/outbound-keyboard.ts +0 -117
  124. package/src/outbound-markdown.ts +0 -212
  125. package/src/outbound-media.ts +0 -58
  126. package/src/outbound-mixed-media.ts +0 -101
  127. package/src/qq-endpoint-manager.ts +0 -199
  128. package/src/sdk-version.ts +0 -21
  129. package/src/segment-mapper.ts +0 -1
  130. package/src/types.ts +0 -52
@@ -0,0 +1,96 @@
1
+ /**
2
+ * QQ Official Bot protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
6
+ export interface QqAdapterConfig {
7
+ readonly name?: string;
8
+ readonly appid?: string;
9
+ readonly secret?: string;
10
+ /** Default `websocket`. `webhook` / `middleware` use httpHostToken POST. */
11
+ readonly mode?: 'websocket' | 'webhook' | 'middleware';
12
+ readonly sandbox?: boolean;
13
+ readonly intents?: readonly string[];
14
+ readonly accessTokenUrl?: string;
15
+ readonly gatewayUrl?: string;
16
+ readonly webhookPath?: string;
17
+ readonly port?: number;
18
+ readonly path?: string;
19
+ /** Transitional: legacy root `endpoints[]` with `context: qq`. */
20
+ readonly endpoints?: ReadonlyArray<{
21
+ readonly context?: string;
22
+ readonly name?: string;
23
+ readonly appid?: string;
24
+ readonly secret?: string;
25
+ readonly mode?: 'websocket' | 'webhook' | 'middleware';
26
+ readonly sandbox?: boolean;
27
+ readonly intents?: readonly string[];
28
+ readonly accessTokenUrl?: string;
29
+ readonly gatewayUrl?: string;
30
+ readonly webhookPath?: string;
31
+ readonly port?: number;
32
+ readonly path?: string;
33
+ }>;
34
+ }
35
+ export interface ResolvedQqWebsocketConfig {
36
+ readonly context: 'qq';
37
+ readonly mode: 'websocket';
38
+ readonly name: string;
39
+ readonly appid: string;
40
+ readonly secret: string;
41
+ readonly sandbox: boolean;
42
+ readonly intents?: readonly string[];
43
+ readonly accessTokenUrl?: string;
44
+ readonly gatewayUrl?: string;
45
+ }
46
+ export interface ResolvedQqHttpConfig {
47
+ readonly context: 'qq';
48
+ readonly mode: 'webhook' | 'middleware';
49
+ readonly name: string;
50
+ readonly appid: string;
51
+ readonly secret: string;
52
+ readonly webhookPath: string;
53
+ readonly sandbox: boolean;
54
+ }
55
+ export type ResolvedQqConfig = ResolvedQqWebsocketConfig | ResolvedQqHttpConfig;
56
+ export type QqChannelKind = 'private' | 'group' | 'channel' | 'direct';
57
+ export interface QqInboundMessage {
58
+ readonly id: string;
59
+ readonly content: string;
60
+ readonly channelKind: QqChannelKind;
61
+ readonly channelId: string;
62
+ readonly authorId: string;
63
+ readonly authorName: string;
64
+ readonly authorRoles?: string[];
65
+ readonly timestamp: number;
66
+ readonly guildId?: string;
67
+ readonly rawMessage?: string;
68
+ /**
69
+ * 入站即 @ 机器人(AT_MESSAGE_CREATE 的 mentions[].bot,或群 GROUP_AT_MESSAGE_CREATE
70
+ * 事件本身仅 @ 时下发)。新 Runtime 纯文本 content 需经 metadata 传递。
71
+ */
72
+ readonly mentioned?: boolean;
73
+ }
74
+ export interface QqWireSegment {
75
+ readonly type: string;
76
+ readonly data?: Record<string, unknown>;
77
+ }
78
+ export interface ParsedSendTarget {
79
+ readonly kind: QqChannelKind;
80
+ readonly id: string;
81
+ }
82
+ export declare function resolveQqConfig(config?: QqAdapterConfig): ResolvedQqConfig;
83
+ /**
84
+ * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
85
+ */
86
+ export declare function formatInboundTarget(msg: QqInboundMessage): string;
87
+ export declare function parseSendTarget(target: string): ParsedSendTarget;
88
+ export declare function senderDisplayName(msg: QqInboundMessage): string;
89
+ export declare function formatInboundContent(msg: QqInboundMessage): string;
90
+ /**
91
+ * Wire-encode an already-rendered outbound payload into a plain text / QQ Sendable string.
92
+ * Segment canonicalization is intentionally not done here.
93
+ */
94
+ export declare function formatOutboundText(payload: unknown): string;
95
+ /** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
96
+ export declare function resolveOutboundMessageId(result: unknown): string;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * QQ Official Bot protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import { pickCredential } from '@zhin.js/adapter';
6
+ export function resolveQqConfig(config = {}) {
7
+ const entry = config.endpoints?.find((item) => item.context === 'qq' || !item.context);
8
+ const appid = pickCredential(config.appid, entry?.appid, process.env.QQ_APPID, process.env.QQ_BOT_APPID);
9
+ const secret = pickCredential(config.secret, entry?.secret, process.env.QQ_SECRET, process.env.QQ_BOT_SECRET);
10
+ if (!appid || !secret) {
11
+ throw new TypeError('QQ adapter requires appid + secret (plugins.<key>.appid/secret or endpoints with context: qq)');
12
+ }
13
+ const name = (typeof config.name === 'string' && config.name)
14
+ || (typeof entry?.name === 'string' && entry.name)
15
+ || process.env.QQ_BOT_NAME
16
+ || 'qq-bot';
17
+ const mode = config.mode ?? entry?.mode ?? 'websocket';
18
+ if (mode === 'webhook' || mode === 'middleware') {
19
+ const webhookPathRaw = config.webhookPath ?? entry?.webhookPath ?? '/qq/webhook';
20
+ const webhookPath = webhookPathRaw.startsWith('/') ? webhookPathRaw : `/${webhookPathRaw}`;
21
+ return {
22
+ context: 'qq',
23
+ mode,
24
+ name,
25
+ appid,
26
+ secret,
27
+ webhookPath,
28
+ sandbox: config.sandbox === true || entry?.sandbox === true,
29
+ };
30
+ }
31
+ return {
32
+ context: 'qq',
33
+ mode: 'websocket',
34
+ name,
35
+ appid,
36
+ secret,
37
+ sandbox: config.sandbox === true || entry?.sandbox === true,
38
+ intents: config.intents ?? entry?.intents,
39
+ accessTokenUrl: config.accessTokenUrl ?? entry?.accessTokenUrl,
40
+ gatewayUrl: config.gatewayUrl ?? entry?.gatewayUrl,
41
+ };
42
+ }
43
+ /**
44
+ * Gateway reply target:`private:uid` / `group:gid` / `channel:cid` / `direct:guildId`.
45
+ */
46
+ export function formatInboundTarget(msg) {
47
+ return `${msg.channelKind}:${msg.channelId}`;
48
+ }
49
+ export function parseSendTarget(target) {
50
+ const sep = target.indexOf(':');
51
+ if (sep <= 0) {
52
+ return { kind: 'private', id: target };
53
+ }
54
+ const head = target.slice(0, sep);
55
+ const rest = target.slice(sep + 1);
56
+ if (head === 'private' || head === 'group' || head === 'channel' || head === 'direct') {
57
+ return { kind: head, id: rest };
58
+ }
59
+ return { kind: 'private', id: target };
60
+ }
61
+ export function senderDisplayName(msg) {
62
+ return msg.authorName || msg.authorId;
63
+ }
64
+ export function formatInboundContent(msg) {
65
+ const text = (msg.content || msg.rawMessage || '').trim();
66
+ return text || '(Empty message)';
67
+ }
68
+ /**
69
+ * Wire-encode an already-rendered outbound payload into a plain text / QQ Sendable string.
70
+ * Segment canonicalization is intentionally not done here.
71
+ */
72
+ export function formatOutboundText(payload) {
73
+ if (typeof payload === 'string')
74
+ return payload;
75
+ const segments = Array.isArray(payload)
76
+ ? payload
77
+ : payload && typeof payload === 'object' && 'type' in payload
78
+ ? [payload]
79
+ : [];
80
+ if (segments.length === 0) {
81
+ return payload == null
82
+ ? ''
83
+ : typeof payload === 'object'
84
+ ? JSON.stringify(payload)
85
+ : String(payload);
86
+ }
87
+ return segments
88
+ .map((item) => {
89
+ if (typeof item === 'string')
90
+ return item;
91
+ const data = item.data ?? {};
92
+ switch (item.type) {
93
+ case 'text':
94
+ return String(data.text ?? data.content ?? '');
95
+ case 'at': {
96
+ const id = String(data.user_id ?? data.qq ?? data.id ?? '');
97
+ return id ? `<@!${id}>` : '';
98
+ }
99
+ case 'image':
100
+ return String(data.url || data.file || '');
101
+ case 'reply':
102
+ return '';
103
+ default:
104
+ return data.text != null ? String(data.text) : '';
105
+ }
106
+ })
107
+ .filter(Boolean)
108
+ .join('');
109
+ }
110
+ /** 从 QQ API SendResult / 审核回包中解析出站消息 ID */
111
+ export function resolveOutboundMessageId(result) {
112
+ if (!result || typeof result !== 'object') {
113
+ throw new Error('QQ 发送消息失败:响应为空');
114
+ }
115
+ const row = result;
116
+ const nested = row.data && typeof row.data === 'object'
117
+ ? row.data
118
+ : undefined;
119
+ const audit = (row.message_audit ?? nested?.message_audit);
120
+ const id = row.id ?? row.message_id ?? audit?.audit_id;
121
+ if (id == null || id === '') {
122
+ const code = row.code;
123
+ const msg = row.message;
124
+ throw new Error(code != null
125
+ ? `QQ 发送消息失败(${String(code)})${msg ? `: ${String(msg)}` : ''}`
126
+ : 'QQ 发送消息失败:响应缺少消息 ID');
127
+ }
128
+ return String(id);
129
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Agent tool deps for qq.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+ export interface QqAgentEndpoint {
6
+ getGuilds(): Promise<unknown[]>;
7
+ getChannels(guildId: string): Promise<unknown[]>;
8
+ getChannelInfo(channelId: string): Promise<unknown>;
9
+ getGuildMember(guildId: string, userId: string): Promise<unknown>;
10
+ getGuildRoles(guildId: string): Promise<unknown[]>;
11
+ createGuildRole(guildId: string, name: string, color?: number): Promise<unknown>;
12
+ addMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
13
+ removeMemberRole(guildId: string, channelId: string, userId: string, roleId: string): Promise<boolean>;
14
+ }
15
+ export interface QqAgentDeps {
16
+ getEndpoint: (endpointId: string) => QqAgentEndpoint;
17
+ }
18
+ export declare function registerQqAgentEndpoint(endpointId: string, endpoint: QqAgentEndpoint): () => void;
19
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
20
+ export declare function setQqAgentDeps(deps: QqAgentDeps | null): void;
21
+ export declare function getQqAgentDeps(): QqAgentDeps;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Agent tool deps for qq.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+ const endpoints = new Map();
6
+ let override = null;
7
+ export function registerQqAgentEndpoint(endpointId, endpoint) {
8
+ endpoints.set(endpointId, endpoint);
9
+ return () => {
10
+ if (endpoints.get(endpointId) === endpoint) {
11
+ endpoints.delete(endpointId);
12
+ }
13
+ };
14
+ }
15
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
16
+ export function setQqAgentDeps(deps) {
17
+ override = deps;
18
+ }
19
+ export function getQqAgentDeps() {
20
+ if (override)
21
+ return override;
22
+ return {
23
+ getEndpoint(endpointId) {
24
+ const registered = endpoints.get(endpointId);
25
+ if (!registered)
26
+ throw new Error(`Endpoint ${endpointId} 不存在`);
27
+ return registered;
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,36 @@
1
+ export type QQBotEnv = 'production' | 'test';
2
+ export declare enum BindStatus {
3
+ NONE = 0,
4
+ PENDING = 1,
5
+ COMPLETED = 2,
6
+ EXPIRED = 3
7
+ }
8
+ export declare function getQQBotHost(env?: QQBotEnv): string;
9
+ export declare function generateBindKey(): string;
10
+ export declare function buildConnectUrl(taskId: string, source?: string): string;
11
+ /**
12
+ * AES-256-GCM 解密 base64 密文。
13
+ * 密文格式: IV(12 bytes) + ciphertext + AuthTag(16 bytes)
14
+ */
15
+ export declare function decryptSecret(encryptedBase64: string, keyBase64: string): string;
16
+ export interface BindTaskResult {
17
+ taskId: string;
18
+ key: string;
19
+ }
20
+ export declare function createBindTask(env?: QQBotEnv, timeoutMs?: number): Promise<BindTaskResult>;
21
+ export interface PollBindResultOk {
22
+ status: BindStatus;
23
+ botAppId: string;
24
+ botEncryptSecret: string;
25
+ /** 扫码绑定开发者 QQ 的 OpenID(poll_bind_result 返回) */
26
+ userOpenId: string;
27
+ }
28
+ export declare function parsePollBindResultData(data: {
29
+ status?: number;
30
+ bot_appid?: string | number;
31
+ bot_encrypt_secret?: string;
32
+ user_openid?: string;
33
+ userOpenid?: string;
34
+ openid?: string;
35
+ } | undefined): PollBindResultOk;
36
+ export declare function pollBindResult(taskId: string, env?: QQBotEnv, timeoutMs?: number): Promise<PollBindResultOk>;
@@ -87,4 +87,3 @@ export async function pollBindResult(taskId, env = 'production', timeoutMs = 10_
87
87
  }
88
88
  return parsePollBindResultData(res.data);
89
89
  }
90
- //# sourceMappingURL=qq-bind-api.js.map
@@ -0,0 +1,19 @@
1
+ export interface QqBindCredentials {
2
+ appId: string;
3
+ appSecret: string;
4
+ userOpenId?: string;
5
+ }
6
+ export interface QqBindCallbacks {
7
+ onSuccess: (credentials: QqBindCredentials[]) => void;
8
+ onFailure: (error: Error) => void;
9
+ onQrDisplayed?: (url: string) => void | Promise<void>;
10
+ onQrExpired?: () => void | Promise<void>;
11
+ }
12
+ export interface QqBindFlowOptions {
13
+ signal?: AbortSignal;
14
+ source?: string;
15
+ }
16
+ /**
17
+ * 轮询等待扫码结果(回调风格)。返回 stop 函数可中止流程。
18
+ */
19
+ export declare function startQqBindFlow(callbacks: QqBindCallbacks, options?: QqBindFlowOptions): () => void;
@@ -83,4 +83,3 @@ export function startQqBindFlow(callbacks, options = {}) {
83
83
  });
84
84
  return () => controller.abort();
85
85
  }
86
- //# sourceMappingURL=qq-bind-flow.js.map
@@ -0,0 +1,12 @@
1
+ export interface QqCredentialEnvKeys {
2
+ appidKey: string;
3
+ secretKey: string;
4
+ appidRef: string;
5
+ secretRef: string;
6
+ }
7
+ /** 按 endpoint 名称生成唯一 env 键(如 `QQ_MY_BOT_APPID`) */
8
+ export declare function buildQqCredentialEnvKeys(endpointName: string): QqCredentialEnvKeys;
9
+ /** 项目根:ZHIN_PROJECT_ROOT 优先,缺省 process.cwd()(替代 legacy runtimeCwd) */
10
+ export declare function resolveProjectRoot(): string;
11
+ /** 写入或更新 `.env` 中的键值,并同步到当前进程 `process.env` */
12
+ export declare function persistQqCredentialsToEnv(endpointName: string, appId: string, appSecret: string, projectRoot?: string): QqCredentialEnvKeys;
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { runtimeCwd } from 'zhin.js';
4
- /** 按 endpoint 名称生成唯一 env 键(如 `QQ_MOCK_QQ_BOT_APPID`) */
3
+ /** 按 endpoint 名称生成唯一 env 键(如 `QQ_MY_BOT_APPID`) */
5
4
  export function buildQqCredentialEnvKeys(endpointName) {
6
5
  const slug = endpointName.replace(/[^a-zA-Z0-9_]/g, '_').toUpperCase();
7
6
  const appidKey = `QQ_${slug}_APPID`;
@@ -13,9 +12,10 @@ export function buildQqCredentialEnvKeys(endpointName) {
13
12
  secretRef: `\${${secretKey}}`,
14
13
  };
15
14
  }
16
- function resolveProjectRoot() {
15
+ /** 项目根:ZHIN_PROJECT_ROOT 优先,缺省 process.cwd()(替代 legacy runtimeCwd) */
16
+ export function resolveProjectRoot() {
17
17
  const envRoot = process.env.ZHIN_PROJECT_ROOT?.trim();
18
- return path.resolve(envRoot || runtimeCwd());
18
+ return path.resolve(envRoot || process.cwd());
19
19
  }
20
20
  function escapeRegExp(text) {
21
21
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -49,4 +49,3 @@ export function persistQqCredentialsToEnv(endpointName, appId, appSecret, projec
49
49
  fs.writeFileSync(envPath, content);
50
50
  return keys;
51
51
  }
52
- //# sourceMappingURL=qq-bind-persist.js.map
@@ -0,0 +1,25 @@
1
+ import type { QqRuntimeState } from './qq-runtime-state.js';
2
+ export type QqCommandReply = (text: string) => Promise<unknown>;
3
+ /**
4
+ * endpoint 管理命令的操作者校验:实例配置声明了 master(顶层或任一端点项)时
5
+ * 仅 master 可执行 add/cancel/remove;未配置则放行(首个扫码绑定者即为 owner,
6
+ * legacy applyBindOwnership 会把 operator 写为新端点的 master)。
7
+ */
8
+ export declare function isQqEndpointOperator(config: unknown, input: unknown): boolean;
9
+ export declare const QQ_ENDPOINT_FORBIDDEN = "\u4EC5 master \u53EF\u6267\u884C QQ endpoint \u7BA1\u7406\u547D\u4EE4";
10
+ /**
11
+ * 从命令 input(Runtime Message)提取 $reply;非消息来源(如 Host API 调用)降级为 no-op。
12
+ */
13
+ export declare function extractQqCommandReply(input: unknown): QqCommandReply;
14
+ /** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
15
+ export declare function runQqEndpointList(state: QqRuntimeState, projectRoot?: string): string;
16
+ /**
17
+ * `qq endpoint add [name]`:启动扫码绑定流程。
18
+ * 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
19
+ * 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
20
+ */
21
+ export declare function runQqEndpointAdd(state: QqRuntimeState, name: string | undefined, reply: QqCommandReply, projectRoot?: string): Promise<string>;
22
+ /** `qq endpoint cancel`:中止进行中的绑定流程 */
23
+ export declare function runQqEndpointCancel(state: QqRuntimeState): string;
24
+ /** `qq endpoint remove <name>`:从 zhin.config.yml 移除对应 endpoints 项 */
25
+ export declare function runQqEndpointRemove(_state: QqRuntimeState, name: string, projectRoot?: string): string;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * `qq endpoint` 命令的业务逻辑(与命令定义文件分离,便于测试)。
3
+ */
4
+ import { startQqBindFlow } from './qq-bind-flow.js';
5
+ import { persistQqCredentialsToEnv } from './qq-bind-persist.js';
6
+ import { addQqEndpointToConfig, listQqEndpointEntries, removeQqEndpointFromConfig, } from './qq-endpoint-config.js';
7
+ /**
8
+ * endpoint 管理命令的操作者校验:实例配置声明了 master(顶层或任一端点项)时
9
+ * 仅 master 可执行 add/cancel/remove;未配置则放行(首个扫码绑定者即为 owner,
10
+ * legacy applyBindOwnership 会把 operator 写为新端点的 master)。
11
+ */
12
+ export function isQqEndpointOperator(config, input) {
13
+ const cfg = (config ?? {});
14
+ const masters = new Set();
15
+ const collect = (value) => {
16
+ if (value === undefined || value === null)
17
+ return;
18
+ const text = String(value).trim();
19
+ if (text)
20
+ masters.add(text);
21
+ };
22
+ collect(cfg.master);
23
+ if (Array.isArray(cfg.endpoints)) {
24
+ for (const entry of cfg.endpoints) {
25
+ collect(entry?.master);
26
+ }
27
+ }
28
+ if (masters.size === 0)
29
+ return true;
30
+ const sender = String(input?.sender ?? '').trim();
31
+ return !!sender && masters.has(sender);
32
+ }
33
+ export const QQ_ENDPOINT_FORBIDDEN = '仅 master 可执行 QQ endpoint 管理命令';
34
+ /**
35
+ * 从命令 input(Runtime Message)提取 $reply;非消息来源(如 Host API 调用)降级为 no-op。
36
+ */
37
+ export function extractQqCommandReply(input) {
38
+ const reply = input?.$reply;
39
+ if (typeof reply === 'function') {
40
+ return (text) => reply.call(input, text);
41
+ }
42
+ return async () => undefined;
43
+ }
44
+ /** `qq endpoint list`:运行中的 endpoints(本 generation adapter create 注册)+ 配置里的 endpoints */
45
+ export function runQqEndpointList(state, projectRoot) {
46
+ const running = [...state.endpoints.values()];
47
+ const configured = listQqEndpointEntries(projectRoot);
48
+ const lines = [];
49
+ lines.push('【运行中的 QQ endpoints】');
50
+ if (running.length === 0) {
51
+ lines.push(' (无)');
52
+ }
53
+ else {
54
+ for (const endpoint of running) {
55
+ lines.push(` - ${endpoint.name}(${endpoint.mode})`);
56
+ }
57
+ }
58
+ lines.push('【配置中的 QQ endpoints】(zhin.config.yml → plugins.qq.endpoints)');
59
+ if (configured.length === 0) {
60
+ lines.push(' (无)');
61
+ }
62
+ else {
63
+ for (const entry of configured) {
64
+ lines.push(` - ${entry.name}(appid: ${entry.appid})`);
65
+ }
66
+ }
67
+ if (state.bindFlow) {
68
+ lines.push('⚠️ 有进行中的扫码绑定,可用 qq endpoint cancel 取消');
69
+ }
70
+ return lines.join('\n');
71
+ }
72
+ /**
73
+ * `qq endpoint add [name]`:启动扫码绑定流程。
74
+ * 返回的 Promise 在二维码链接就绪(或前置失败)时 resolve 为首条回复;
75
+ * 后续状态(已扫码 / 成功 / 失败 / 过期刷新)通过 reply 推回当前会话。
76
+ */
77
+ export function runQqEndpointAdd(state, name, reply, projectRoot) {
78
+ if (state.bindFlow) {
79
+ return Promise.resolve('已有进行中的 QQ 机器人绑定,请先发送 qq endpoint cancel 取消后再试');
80
+ }
81
+ const endpointName = name?.trim() || undefined;
82
+ return new Promise((resolve) => {
83
+ let firstReplied = false;
84
+ const settle = (text) => {
85
+ if (!firstReplied) {
86
+ firstReplied = true;
87
+ resolve(text);
88
+ return;
89
+ }
90
+ void reply(text);
91
+ };
92
+ const stop = startQqBindFlow({
93
+ onQrDisplayed: (url) => {
94
+ // QQ 出站当前仅支持纯文本(富媒体未迁移),二维码只能发链接文本
95
+ settle(`请用手机 QQ 打开以下链接完成扫码绑定(二维码图片出站暂未支持,故发送链接):\n${url}`);
96
+ },
97
+ onQrExpired: async () => {
98
+ await reply('二维码已过期,正在刷新,请扫描新链接…');
99
+ },
100
+ onSuccess: async (credentials) => {
101
+ state.bindFlow = null;
102
+ try {
103
+ const [{ appId, appSecret }] = credentials;
104
+ const finalName = endpointName ?? appId;
105
+ const envKeys = persistQqCredentialsToEnv(finalName, appId, appSecret, projectRoot);
106
+ const filePath = addQqEndpointToConfig({ name: finalName, appid: envKeys.appidRef, secret: envKeys.secretRef }, projectRoot);
107
+ settle(`✅ 绑定成功!endpoint「${finalName}」的凭据已写入 .env,并已追加到 ${filePath} 的 plugins.qq.endpoints。\n` +
108
+ '⚠️ 需重启 zhin 后新 endpoint 才会生效。');
109
+ }
110
+ catch (error) {
111
+ settle(`绑定成功但写入配置失败:${error instanceof Error ? error.message : String(error)}`);
112
+ }
113
+ },
114
+ onFailure: (error) => {
115
+ state.bindFlow = null;
116
+ settle(`❌ QQ 绑定失败:${error.message}`);
117
+ },
118
+ }, { source: 'zhin' });
119
+ state.bindFlow = { name: endpointName, stop };
120
+ });
121
+ }
122
+ /** `qq endpoint cancel`:中止进行中的绑定流程 */
123
+ export function runQqEndpointCancel(state) {
124
+ if (!state.bindFlow) {
125
+ return '当前没有进行中的 QQ 绑定流程';
126
+ }
127
+ state.bindFlow.stop();
128
+ state.bindFlow = null;
129
+ return '已取消进行中的 QQ 绑定流程';
130
+ }
131
+ /** `qq endpoint remove <name>`:从 zhin.config.yml 移除对应 endpoints 项 */
132
+ export function runQqEndpointRemove(_state, name, projectRoot) {
133
+ const trimmed = name.trim();
134
+ if (!trimmed)
135
+ return '用法:qq endpoint remove <name>';
136
+ try {
137
+ const { removed, filePath } = removeQqEndpointFromConfig(trimmed, projectRoot);
138
+ if (!removed) {
139
+ return `配置中不存在 qq endpoint「${trimmed}」(${filePath} → plugins.qq.endpoints)`;
140
+ }
141
+ return (`已从 ${filePath} 的 plugins.qq.endpoints 移除「${trimmed}」。\n` +
142
+ '⚠️ 需重启 zhin 后生效(运行中的连接届时才会断开);.env 中的凭据键未删除,可手动清理。');
143
+ }
144
+ catch (error) {
145
+ return `移除失败:${error instanceof Error ? error.message : String(error)}`;
146
+ }
147
+ }
@@ -0,0 +1,17 @@
1
+ export interface QqEndpointConfigEntry {
2
+ name: string;
3
+ appid: string;
4
+ secret: string;
5
+ [key: string]: unknown;
6
+ }
7
+ /** 定位项目配置文件:ZHIN_CONFIG 指定优先,否则发现 zhin.config.yml/.yaml,都没有则默认新建 zhin.config.yml */
8
+ export declare function findQqConfigFile(projectRoot?: string): string;
9
+ /** 读取 plugins.qq.endpoints(plain JS);plugins/qq 缺失或形态不符时返回 [] */
10
+ export declare function listQqEndpointEntries(projectRoot?: string): QqEndpointConfigEntry[];
11
+ /** 追加 endpoint 到 plugins.qq.endpoints;name 已存在时报错 */
12
+ export declare function addQqEndpointToConfig(entry: QqEndpointConfigEntry, projectRoot?: string): string;
13
+ /** 按 name 移除 plugins.qq.endpoints 项;不存在返回 false */
14
+ export declare function removeQqEndpointFromConfig(name: string, projectRoot?: string): {
15
+ removed: boolean;
16
+ filePath: string;
17
+ };