@zhin.js/adapter-icqq 1.1.20 → 1.1.21

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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 1.1.21
4
+
5
+ ### Patch Changes
6
+
7
+ - af8f803: Use the current Endpoint's platform profile for Agent account identity, with per-field configured fallbacks and scoped, identity-approved profile tools. Keep SOUL first-person and independent of account names. Profile ports preserve the source Client, cancellation, truthful update outcomes and private image authority.
8
+
9
+ Adapter Feature API 2.1 and Tool Feature API 1.1 describe the additive profile requirements; new Core, official adapters and generated plugins declare the corresponding floors. Existing compatible API-2.0/API-1.0 plugins remain accepted by the newer providers. npm releases remain patch.
10
+
11
+ Private IM assets now bind to the full canonical conversation and principal. Existing IM asset records that contain only the earlier lossy hash remain on disk until their original expiry, but cannot be safely rebound; recapture them. No automatic file deletion, platform profile mutation or workspace SOUL rewrite occurs during upgrade.
12
+
13
+ - Updated dependencies [b88e988]
14
+ - Updated dependencies [f78eb84]
15
+ - Updated dependencies [c15cef9]
16
+ - Updated dependencies [b0c02a2]
17
+ - Updated dependencies [af8f803]
18
+ - Updated dependencies [26fda97]
19
+ - Updated dependencies [976886c]
20
+ - Updated dependencies [032dc96]
21
+ - @zhin.js/adapter@1.1.27
22
+ - @zhin.js/client@1.1.23
23
+ - @zhin.js/im-contract@1.1.10
24
+ - @zhin.js/core@1.1.55
25
+ - @zhin.js/tool@1.1.14
26
+ - @zhin.js/page@1.1.13
27
+ - @zhin.js/agent-feature@1.1.12
28
+ - @zhin.js/feature-kit@1.1.11
29
+ - @zhin.js/permission@1.1.11
30
+ - @zhin.js/skill@1.1.12
31
+
3
32
  ## 1.1.20
4
33
 
5
34
  ### Patch Changes
package/README.md CHANGED
@@ -249,3 +249,13 @@ ICQQ 要求分享、文件、语音、视频、JSON 卡片和合并转发单独
249
249
  Endpoint 管理和工具反馈跟随当前操作语言;登录没有当前聊天发送者,LoginAssist 提示采用项目语言。提示文案变化不改变扫码、短信、滑块验证过程,群管理权限、审批与平台结果检查保持原规则。
250
250
 
251
251
  Endpoint 添加用法中的调用头由当前环境的实际命令前缀与插件命名空间提供;未知时只提示所需参数,不生成猜测命令。
252
+
253
+ ## 当前账号资料
254
+
255
+ `endpointSpec.profile` 接到原生 ICQQ Client 的资料卡与设置方法;Agent 通过统一的 `get_self_profile` / `set_self_profile` 工具访问当前账号,修改沿用身份类审批。平台资料优先于配置兜底,群名片仍是独立的场景资料。
256
+
257
+ - 可读:昵称、性别、年龄、个性签名、头像 URL。只投影这些公开资料,不把 SDK 返回的邮箱、QID 等一起交给模型。
258
+ - 可写:昵称、性别、签名、个人说明、头像。SDK 返回 `false` 时明确拒绝,不报告成功。头像输入经工具链解析为已授权图片,结果不回传图片 base64。
259
+ - 年龄不反推生日;当前安装的 ICQQ 1.12.3 `setBirthday` 月日编码有缺陷,因此不开放生日写入。兴趣无对应已验证接口,只能使用配置兜底。
260
+
261
+ 配置的 `nickname`、`profile` 可放在适配器默认值或某个 `endpoints[]` 项中;Endpoint 逐字段覆盖默认值,只有平台未提供的字段才使用它们。配置兜底不会写回 QQ。
@@ -15,9 +15,9 @@ tools:
15
15
  ---
16
16
  # icqq-conversation
17
17
 
18
- Use `conversation_info` before claiming the bot's role or available platform permissions. It uses the current endpoint account, not the sender's role. Group roles are refreshed from the platform on demand; failure is not evidence that the bot is an ordinary member.
18
+ Use `conversation_info` before claiming your role or available platform permissions. It uses the current endpoint account, not the sender's role. Group roles are refreshed from the platform on demand; failure is not evidence that you are an ordinary member.
19
19
 
20
- Caller roles authorize a request; bot platform roles determine whether QQ can execute it. They are independent. A successful inspection does not replace the live permission check before a management action.
20
+ Caller roles authorize a request; your platform roles determine whether QQ can execute it. They are independent. A successful inspection does not replace the live permission check before a management action.
21
21
 
22
22
  Guild member lookup can be incomplete. Missing self membership means `unknown`. A `channel_admin` record does not identify which child channels it governs, so it cannot authorize an operation in the current channel.
23
23
 
@@ -3,7 +3,7 @@ import { defineAgentTool } from '@zhin.js/tool';
3
3
  import { z } from 'zod';
4
4
  import { currentConversation } from '../../../../conversation.js';
5
5
  export default defineAgentTool({
6
- description: '读取当前 ICQQ 会话及机器人自己的群/频道身份;区分调用者权限、机器人平台权限和不支持的操作',
6
+ description: '读取当前 ICQQ 会话及你自己的群/频道身份;区分调用者权限、你的平台权限和不支持的操作',
7
7
  adapter: 'icqq',
8
8
  requiresApproval: 'never',
9
9
  inputSchema: z.object({}),
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  import { currentConversation } from '../../../../conversation.js';
4
4
 
5
5
  export default defineAgentTool({
6
- description: '读取当前 ICQQ 会话及机器人自己的群/频道身份;区分调用者权限、机器人平台权限和不支持的操作',
6
+ description: '读取当前 ICQQ 会话及你自己的群/频道身份;区分调用者权限、你的平台权限和不支持的操作',
7
7
  adapter: 'icqq',
8
8
  requiresApproval: 'never',
9
9
  inputSchema: z.object({}),
@@ -35,8 +35,8 @@ scopes:
35
35
 
36
36
  已配置的自动审核流程可对入群 10 分钟内的明确广告直接踢出;该例外由审核流程核验,不授权本技能或普通对话绕过人工审批。
37
37
 
38
- `group_leave` 仅主人(master)可发起,每次人工审批,只能退出当前群。Bot 为群主时拒绝执行,因为 SDK 的退群会解散群。
38
+ `group_leave` 仅主人(master)可发起,每次人工审批,只能退出当前群。你为群主时拒绝执行,因为 SDK 的退群会解散群。
39
39
 
40
- `recall_member_message` 沿用 master/admin/trusted/owner 发起权限,无需逐次审批;目标固定在当前群中的真实消息。执行前实时查询 Bot 与目标成员角色:管理员只能撤回普通成员,群主可撤回管理员或普通成员。平台时限与执行结果仍然有效,不自动重试未确认的结果。
40
+ `recall_member_message` 沿用 master/admin/trusted/owner 发起权限,无需逐次审批;目标固定在当前群中的真实消息。执行前实时查询你与目标成员的角色:管理员只能撤回普通成员,群主可撤回管理员或普通成员。平台时限与执行结果仍然有效,不自动重试未确认的结果。
41
41
 
42
- 撤回 Bot 自己的消息不要求群管角色:使用 `icqq-messages` 的 `recall_own_message`。已配置自动群审核的撤回继续走原有审核路径,不因本工具新增审批,也不扩大审核类别或目标群。
42
+ 撤回你自己的消息不要求群管角色:使用 `icqq-messages` 的 `recall_own_message`。已配置自动群审核的撤回继续走原有审核路径,不因本工具新增审批,也不扩大审核类别或目标群。
@@ -4,7 +4,7 @@ import { z } from 'zod';
4
4
  import { currentGroup } from '../../../../conversation.js';
5
5
  import { messages as t } from '../../../../messages.js';
6
6
  export default defineAgentTool({
7
- description: '让机器人退出当前 QQ 群;仅主人可发起且每次人工审批,机器人是群主时拒绝执行以免解散群',
7
+ description: '退出你当前所在的 QQ 群;仅主人可发起且每次人工审批,你是群主时拒绝执行以免解散群',
8
8
  adapter: 'icqq',
9
9
  scopes: ['group'],
10
10
  currentConversationBotRoles: ['admin', 'member'],
@@ -4,7 +4,7 @@ import { currentGroup } from '../../../../conversation.js';
4
4
  import { messages as t } from '../../../../messages.js';
5
5
 
6
6
  export default defineAgentTool({
7
- description: '让机器人退出当前 QQ 群;仅主人可发起且每次人工审批,机器人是群主时拒绝执行以免解散群',
7
+ description: '退出你当前所在的 QQ 群;仅主人可发起且每次人工审批,你是群主时拒绝执行以免解散群',
8
8
  adapter: 'icqq',
9
9
  scopes: ['group'],
10
10
  currentConversationBotRoles: ['admin', 'member'],
@@ -4,7 +4,7 @@ import { z } from 'zod';
4
4
  import { currentGroup, recallMessage, resolveMessage } from '../../../../conversation.js';
5
5
  import { messages as t } from '../../../../messages.js';
6
6
  export default defineAgentTool({
7
- description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及机器人具备实时平台权限,无需逐次审批',
7
+ description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及你具备实时平台权限,无需逐次审批',
8
8
  adapter: 'icqq',
9
9
  scopes: ['group'],
10
10
  currentConversationBotRoles: ['owner', 'admin'],
@@ -4,7 +4,7 @@ import { currentGroup, recallMessage, resolveMessage } from '../../../../convers
4
4
  import { messages as t } from '../../../../messages.js';
5
5
 
6
6
  export default defineAgentTool({
7
- description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及机器人具备实时平台权限,无需逐次审批',
7
+ description: '撤回当前 QQ 群内他人的真实消息;需调用方具备群管发起权限及你具备实时平台权限,无需逐次审批',
8
8
  adapter: 'icqq',
9
9
  scopes: ['group'],
10
10
  currentConversationBotRoles: ['owner', 'admin'],
@@ -23,6 +23,6 @@ Only use real message IDs from the current conversation. Do not invent IDs, sour
23
23
 
24
24
  `quote_reply` sends a text reply quoting one original; `forward_messages` sends a merged forward of the supplied originals. Use `inspect_conversation_reference` to read an incoming quote/forward when available; it does not send messages.
25
25
 
26
- `recall_own_message` needs no management role or per-call approval, including when the bot is an ordinary group member. It verifies the bot authored the message. The platform decides the recall time window; never promise success before receiving confirmation or retry an unconfirmed result automatically. To recall another member's message, load `icqq-group-admin` instead.
26
+ `recall_own_message` needs no management role or per-call approval, including when you are an ordinary group member. It verifies that you authored the message. The platform decides the recall time window; never promise success before receiving confirmation or retry an unconfirmed result automatically. To recall another member's message, load `icqq-group-admin` instead.
27
27
 
28
28
  These tools currently support QQ private/group conversations, not guild channels. A failed or suppressed outbound receipt is not a delivered message.
@@ -4,7 +4,7 @@ import { z } from 'zod';
4
4
  import { recallMessage, resolveMessage } from '../../../../conversation.js';
5
5
  import { messages as t } from '../../../../messages.js';
6
6
  export default defineAgentTool({
7
- description: '撤回机器人在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
7
+ description: '撤回你在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
8
8
  adapter: 'icqq',
9
9
  scopes: ['private', 'group'],
10
10
  requiresApproval: 'never',
@@ -4,7 +4,7 @@ import { recallMessage, resolveMessage } from '../../../../conversation.js';
4
4
  import { messages as t } from '../../../../messages.js';
5
5
 
6
6
  export default defineAgentTool({
7
- description: '撤回机器人在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
7
+ description: '撤回你在当前 QQ 群或私聊中自己的发言;无需群管角色或逐次审批,时限由平台决定',
8
8
  adapter: 'icqq',
9
9
  scopes: ['private', 'group'],
10
10
  requiresApproval: 'never',
package/lib/endpoint.js CHANGED
@@ -15,6 +15,7 @@ import { formatInboundContent, formatOutboundMessageParts, normalizeIcqqImageMed
15
15
  import { ManagedIcqqClient } from './managed-client.js';
16
16
  import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
17
17
  import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
18
+ import { createIcqqSelfProfile } from './self-profile.js';
18
19
  const BOUND_EVENTS = [
19
20
  'message.private',
20
21
  'message.group',
@@ -163,7 +164,7 @@ export class IcqqEndpoint extends ManagedIcqqClient {
163
164
  start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(), stop: () => this.#stopFramework(),
164
165
  send: request => this.#sendFramework(request), get name() { return name(); },
165
166
  get transportState() { return state(); },
166
- management: this.#management, control: this.#control,
167
+ management: this.#management, control: this.#control, profile: createIcqqSelfProfile(this),
167
168
  get content() { return content(); },
168
169
  });
169
170
  this.#contentResolver = new IcqqContentResolver(this);
package/lib/protocol.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
1
2
  import type { Sendable } from '@icqqjs/icqq';
2
3
  import { type ConversationRef } from '@zhin.js/im-contract';
3
4
  import { type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
@@ -163,7 +164,7 @@ export declare const Actions: {
163
164
  readonly GET_NOTIFY: "get_notify";
164
165
  };
165
166
  /** One expanded ICQQ endpoint configuration produced by AdapterIndex. */
166
- export interface IcqqEndpointConfig {
167
+ export interface IcqqEndpointConfig extends EndpointProfileConfiguration {
167
168
  readonly id: string;
168
169
  readonly autoReconnect?: boolean;
169
170
  readonly outboundMedia?: 'file' | 'base64';
@@ -0,0 +1,6 @@
1
+ import type { Client } from '@icqqjs/icqq';
2
+ import { type EndpointProfile } from 'zhin.js/adapter';
3
+ type IcqqProfileClient = Pick<Client, 'uin' | 'nickname' | 'isOnline' | 'getProfile' | 'setNickname' | 'setGender' | 'setSignature' | 'setDescription' | 'setAvatar'>;
4
+ /** ICQQ owns the platform facts; config fallbacks and call leases belong to Adapter. */
5
+ export declare function createIcqqSelfProfile(client: IcqqProfileClient): EndpointProfile;
6
+ export {};
@@ -0,0 +1,56 @@
1
+ import { createEndpointOperation } from 'zhin.js/adapter';
2
+ /** ICQQ owns the platform facts; config fallbacks and call leases belong to Adapter. */
3
+ export function createIcqqSelfProfile(client) {
4
+ return Object.freeze({
5
+ writableFields: Object.freeze(['nickname', 'gender', 'signature', 'bio', 'avatar']),
6
+ get current() { return client.isOnline() ? { nickname: client.nickname } : {}; },
7
+ async read({ signal }) {
8
+ const raw = await createEndpointOperation({ signal }).prepare(() => client.getProfile(client.uin));
9
+ const profile = {
10
+ ...(typeof raw.nickname === 'string' ? { nickname: raw.nickname } : {}),
11
+ ...(raw.sex === 'male' || raw.sex === 'female' || raw.sex === 'unknown' ? { gender: raw.sex } : {}),
12
+ ...(typeof raw.age === 'number' && Number.isFinite(raw.age) ? { age: raw.age } : {}),
13
+ ...(typeof raw.signature === 'string' ? { signature: raw.signature } : {}),
14
+ ...(typeof raw.avatar?.url === 'string' ? { avatar: raw.avatar.url } : {}),
15
+ };
16
+ return profile;
17
+ },
18
+ async write({ change, signal }) {
19
+ const operation = createEndpointOperation({ signal });
20
+ let accepted;
21
+ switch (change.field) {
22
+ case 'nickname':
23
+ accepted = await operation.run(() => client.setNickname(change.value));
24
+ break;
25
+ case 'gender': {
26
+ if (change.value === 'other')
27
+ return { status: 'rejected', message: 'ICQQ supports male, female or unknown gender' };
28
+ const gender = change.value === 'male' ? 1 : change.value === 'female' ? 2 : 0;
29
+ accepted = await operation.run(() => client.setGender(gender));
30
+ break;
31
+ }
32
+ case 'signature':
33
+ accepted = await operation.run(() => client.setSignature(change.value));
34
+ break;
35
+ case 'bio':
36
+ accepted = await operation.run(() => client.setDescription(change.value));
37
+ break;
38
+ case 'avatar': {
39
+ if (change.value.kind === 'file') {
40
+ return { status: 'rejected', message: 'ICQQ avatar requires resolved image bytes, URL or local path' };
41
+ }
42
+ const image = change.value.kind === 'base64' ? Buffer.from(change.value.value, 'base64') : change.value.value;
43
+ await operation.run(() => client.setAvatar(image));
44
+ // The input is not the platform CDN URL; it must not become an observed avatar.
45
+ return { status: 'updated' };
46
+ }
47
+ default: return { status: 'rejected', message: `ICQQ cannot safely update ${change.field}` };
48
+ }
49
+ if (accepted && change.field === 'nickname')
50
+ client.nickname = change.value;
51
+ return accepted
52
+ ? { status: 'updated', profile: { [change.field]: change.value } }
53
+ : { status: 'rejected', message: `ICQQ rejected the ${change.field} change` };
54
+ },
55
+ });
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -33,27 +33,27 @@
33
33
  "directory": "plugins/adapters/icqq"
34
34
  },
35
35
  "dependencies": {
36
- "@zhin.js/adapter": "1.1.26",
37
- "@zhin.js/agent-feature": "1.1.11",
38
- "@zhin.js/client": "1.1.22",
36
+ "@zhin.js/adapter": "1.1.27",
37
+ "@zhin.js/agent-feature": "1.1.12",
38
+ "@zhin.js/client": "1.1.23",
39
39
  "@zhin.js/console-contract": "1.1.3",
40
- "@zhin.js/core": "1.1.54",
41
- "@zhin.js/feature-kit": "1.1.10",
42
- "@zhin.js/im-contract": "1.1.9",
40
+ "@zhin.js/core": "1.1.55",
41
+ "@zhin.js/feature-kit": "1.1.11",
42
+ "@zhin.js/im-contract": "1.1.10",
43
43
  "@zhin.js/logger": "1.1.2",
44
- "@zhin.js/page": "1.1.12",
45
- "@zhin.js/permission": "1.1.10",
46
- "@zhin.js/skill": "1.1.11",
47
- "@zhin.js/tool": "1.1.13"
44
+ "@zhin.js/page": "1.1.13",
45
+ "@zhin.js/permission": "1.1.11",
46
+ "@zhin.js/skill": "1.1.12",
47
+ "@zhin.js/tool": "1.1.14"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@icqqjs/icqq": "^1.12.2",
51
51
  "zod": "^4.0.0",
52
- "@zhin.js/adapter": "^1.1.26",
53
- "@zhin.js/command": "^1.1.12",
54
- "@zhin.js/core": "^1.1.54",
55
- "@zhin.js/prompt-section": "^1.1.10",
56
- "zhin.js": "^1.1.19"
52
+ "@zhin.js/adapter": "^1.1.27",
53
+ "@zhin.js/command": "^1.1.13",
54
+ "@zhin.js/core": "^1.1.55",
55
+ "@zhin.js/prompt-section": "^1.1.11",
56
+ "zhin.js": "^1.1.20"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@icqqjs/icqq": {
@@ -78,8 +78,8 @@
78
78
  "typescript": "^6.0.3",
79
79
  "vitest": "^4.1.10",
80
80
  "zod": "^4.4.3",
81
- "@zhin.js/prompt-section": "1.1.10",
82
- "zhin.js": "1.1.19"
81
+ "@zhin.js/prompt-section": "1.1.11",
82
+ "zhin.js": "1.1.20"
83
83
  },
84
84
  "files": [
85
85
  "adapters",
@@ -111,7 +111,7 @@
111
111
  "features": [
112
112
  {
113
113
  "package": "@zhin.js/adapter",
114
- "api": "^2.0.0"
114
+ "api": "^2.1.0"
115
115
  },
116
116
  {
117
117
  "package": "@zhin.js/command",
@@ -119,7 +119,7 @@
119
119
  },
120
120
  {
121
121
  "package": "@zhin.js/tool",
122
- "api": "^1.0.0"
122
+ "api": "^1.1.0"
123
123
  },
124
124
  {
125
125
  "package": "@zhin.js/skill",
@@ -4,7 +4,7 @@ export default defineAgentPromptSection({
4
4
  title: 'ICQQ / QQ',
5
5
  content: [
6
6
  'Use the current Skill catalog; discover the intent and load the matching Skill before its private Tool.',
7
- 'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
7
+ 'The current IM operation selects your current endpoint Client automatically; never guess user_id or group_id.',
8
8
  ].join('\n'),
9
9
  layer: 'tools',
10
10
  order: 70,
@@ -4,7 +4,7 @@ export default defineAgentPromptSection({
4
4
  title: 'ICQQ / QQ',
5
5
  content: [
6
6
  'Use the current Skill catalog; discover the intent and load the matching Skill before its private Tool.',
7
- 'The current IM operation selects the bot Client automatically; never guess user_id or group_id.',
7
+ 'The current IM operation selects your current endpoint Client automatically; never guess user_id or group_id.',
8
8
  ].join('\n'),
9
9
  layer: 'tools',
10
10
  order: 70,
package/schema.json CHANGED
@@ -433,6 +433,27 @@
433
433
  "endpoint",
434
434
  "id"
435
435
  ]
436
+ },
437
+ "nickname": {
438
+ "type": [
439
+ "string",
440
+ "null"
441
+ ],
442
+ "description": "账号昵称兜底",
443
+ "x-i18n": {
444
+ "en": {
445
+ "description": "Fallback account nickname"
446
+ }
447
+ }
448
+ },
449
+ "profile": {
450
+ "$ref": "#/$defs/selfProfileFallback",
451
+ "description": "账号资料兜底",
452
+ "x-i18n": {
453
+ "en": {
454
+ "description": "Fallback account profile"
455
+ }
456
+ }
436
457
  }
437
458
  },
438
459
  "required": [
@@ -541,10 +562,94 @@
541
562
  "endpoint",
542
563
  "id"
543
564
  ]
565
+ },
566
+ "nickname": {
567
+ "type": [
568
+ "string",
569
+ "null"
570
+ ],
571
+ "description": "账号昵称兜底",
572
+ "x-i18n": {
573
+ "en": {
574
+ "description": "Fallback account nickname"
575
+ }
576
+ }
577
+ },
578
+ "profile": {
579
+ "$ref": "#/$defs/selfProfileFallback",
580
+ "description": "账号资料兜底",
581
+ "x-i18n": {
582
+ "en": {
583
+ "description": "Fallback account profile"
584
+ }
585
+ }
544
586
  }
545
587
  },
546
588
  "required": [
547
589
  "master",
548
590
  "endpoints"
549
- ]
591
+ ],
592
+ "$defs": {
593
+ "selfProfileFallback": {
594
+ "type": "object",
595
+ "additionalProperties": false,
596
+ "properties": {
597
+ "gender": {
598
+ "type": [
599
+ "string",
600
+ "null"
601
+ ],
602
+ "enum": [
603
+ "male",
604
+ "female",
605
+ "other",
606
+ "unknown",
607
+ null
608
+ ]
609
+ },
610
+ "age": {
611
+ "type": [
612
+ "integer",
613
+ "null"
614
+ ],
615
+ "minimum": 0
616
+ },
617
+ "birthday": {
618
+ "type": [
619
+ "string",
620
+ "null"
621
+ ],
622
+ "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$"
623
+ },
624
+ "signature": {
625
+ "type": [
626
+ "string",
627
+ "null"
628
+ ]
629
+ },
630
+ "avatar": {
631
+ "type": [
632
+ "string",
633
+ "null"
634
+ ],
635
+ "pattern": "^(https?://[^\\s/?#]+(?:[/?#].*)?|)$"
636
+ },
637
+ "interests": {
638
+ "type": [
639
+ "array",
640
+ "null"
641
+ ],
642
+ "items": {
643
+ "type": "string"
644
+ }
645
+ },
646
+ "bio": {
647
+ "type": [
648
+ "string",
649
+ "null"
650
+ ]
651
+ }
652
+ }
653
+ }
654
+ }
550
655
  }
package/src/endpoint.ts CHANGED
@@ -79,6 +79,7 @@ import type { SystemMessage as IcqqSystemMessage } from './types.js';
79
79
  import { ManagedIcqqClient, type IcqqBackgroundOperation } from './managed-client.js';
80
80
  import { inspectGroupMember, moderateGroupMember } from './group-moderation.js';
81
81
  import { inspectGroupMemberProfile, inspectGroupScene } from './group-scene.js';
82
+ import { createIcqqSelfProfile } from './self-profile.js';
82
83
 
83
84
  export interface IcqqEndpointOptions {
84
85
  readonly id: CapabilityId;
@@ -239,7 +240,7 @@ export class IcqqEndpoint extends ManagedIcqqClient implements Endpoint {
239
240
  start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(), stop: () => this.#stopFramework(),
240
241
  send: request => this.#sendFramework(request), get name() { return name(); },
241
242
  get transportState() { return state(); },
242
- management: this.#management, control: this.#control,
243
+ management: this.#management, control: this.#control, profile: createIcqqSelfProfile(this),
243
244
  get content() { return content(); },
244
245
  });
245
246
  this.#contentResolver = new IcqqContentResolver(this);
package/src/protocol.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
1
2
  import type { MessageElem, Sendable, ShareElem } from '@icqqjs/icqq';
2
3
  import { isMediaRef, type ConversationRef } from '@zhin.js/im-contract';
3
4
  import { resolveAdapterNotifyTarget, type AdapterNotifyTarget, type ResolvedAdapterNotifyTarget } from 'zhin.js/adapter';
@@ -232,7 +233,7 @@ export const Actions = {
232
233
  } as const;
233
234
 
234
235
  /** One expanded ICQQ endpoint configuration produced by AdapterIndex. */
235
- export interface IcqqEndpointConfig {
236
+ export interface IcqqEndpointConfig extends EndpointProfileConfiguration {
236
237
  readonly id: string;
237
238
  readonly autoReconnect?: boolean;
238
239
  readonly outboundMedia?: 'file' | 'base64';
@@ -0,0 +1,55 @@
1
+ import type { Client } from '@icqqjs/icqq';
2
+ import { createEndpointOperation, type EndpointProfile } from 'zhin.js/adapter';
3
+ import type { SelfProfile } from '@zhin.js/im-contract';
4
+
5
+ type IcqqProfileClient = Pick<Client,
6
+ 'uin' | 'nickname' | 'isOnline' | 'getProfile' | 'setNickname' | 'setGender' |
7
+ 'setSignature' | 'setDescription' | 'setAvatar'>;
8
+
9
+ /** ICQQ owns the platform facts; config fallbacks and call leases belong to Adapter. */
10
+ export function createIcqqSelfProfile(client: IcqqProfileClient): EndpointProfile {
11
+ return Object.freeze<EndpointProfile>({
12
+ writableFields: Object.freeze(['nickname', 'gender', 'signature', 'bio', 'avatar']),
13
+ get current() { return client.isOnline() ? { nickname: client.nickname } : {}; },
14
+ async read({ signal }) {
15
+ const raw = await createEndpointOperation({ signal }).prepare(() => client.getProfile(client.uin));
16
+ const profile: SelfProfile = {
17
+ ...(typeof raw.nickname === 'string' ? { nickname: raw.nickname } : {}),
18
+ ...(raw.sex === 'male' || raw.sex === 'female' || raw.sex === 'unknown' ? { gender: raw.sex } : {}),
19
+ ...(typeof raw.age === 'number' && Number.isFinite(raw.age) ? { age: raw.age } : {}),
20
+ ...(typeof raw.signature === 'string' ? { signature: raw.signature } : {}),
21
+ ...(typeof raw.avatar?.url === 'string' ? { avatar: raw.avatar.url } : {}),
22
+ };
23
+ return profile;
24
+ },
25
+ async write({ change, signal }) {
26
+ const operation = createEndpointOperation({ signal });
27
+ let accepted: boolean;
28
+ switch (change.field) {
29
+ case 'nickname': accepted = await operation.run(() => client.setNickname(change.value)); break;
30
+ case 'gender': {
31
+ if (change.value === 'other') return { status: 'rejected', message: 'ICQQ supports male, female or unknown gender' };
32
+ const gender = change.value === 'male' ? 1 : change.value === 'female' ? 2 : 0;
33
+ accepted = await operation.run(() => client.setGender(gender));
34
+ break;
35
+ }
36
+ case 'signature': accepted = await operation.run(() => client.setSignature(change.value)); break;
37
+ case 'bio': accepted = await operation.run(() => client.setDescription(change.value)); break;
38
+ case 'avatar': {
39
+ if (change.value.kind === 'file') {
40
+ return { status: 'rejected', message: 'ICQQ avatar requires resolved image bytes, URL or local path' };
41
+ }
42
+ const image = change.value.kind === 'base64' ? Buffer.from(change.value.value, 'base64') : change.value.value;
43
+ await operation.run(() => client.setAvatar(image));
44
+ // The input is not the platform CDN URL; it must not become an observed avatar.
45
+ return { status: 'updated' };
46
+ }
47
+ default: return { status: 'rejected', message: `ICQQ cannot safely update ${change.field}` };
48
+ }
49
+ if (accepted && change.field === 'nickname') client.nickname = change.value;
50
+ return accepted
51
+ ? { status: 'updated', profile: { [change.field]: change.value } }
52
+ : { status: 'rejected', message: `ICQQ rejected the ${change.field} change` };
53
+ },
54
+ });
55
+ }