@zhin.js/adapter-github 5.0.2 → 5.0.5

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,95 @@
1
1
  # @zhin.js/adapter-github
2
2
 
3
+ ## 5.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 36c7400: Replace `AgentPromptBuildContext.commMessage` with an authenticated platform projection and make Agent prompt assembly consume canonical Turn identity. Platform contributors and prompt hooks no longer receive a classic IM `Message`.
8
+
9
+ Remove the unused Message field and active-context escape hatch from PromptController; turn scheduling is keyed only by canonical session identity.
10
+
11
+ - Updated dependencies [6f9c366]
12
+ - Updated dependencies [373a56b]
13
+ - Updated dependencies [0de46a8]
14
+ - Updated dependencies [c106ecc]
15
+ - Updated dependencies [ca92e03]
16
+ - Updated dependencies [b0f37ae]
17
+ - Updated dependencies [ba08a2f]
18
+ - Updated dependencies [b08f7fe]
19
+ - Updated dependencies [daffd4c]
20
+ - Updated dependencies [36c7400]
21
+ - Updated dependencies [a9fa72e]
22
+ - Updated dependencies [c8de3ef]
23
+ - Updated dependencies [60f0fc8]
24
+ - Updated dependencies [574c990]
25
+ - Updated dependencies [d096f16]
26
+ - Updated dependencies [3f29623]
27
+ - Updated dependencies [2916852]
28
+ - Updated dependencies [d047869]
29
+ - Updated dependencies [162fa34]
30
+ - Updated dependencies [e62561e]
31
+ - Updated dependencies [3eeeb46]
32
+ - Updated dependencies [85d0f82]
33
+ - Updated dependencies [61bfc1c]
34
+ - Updated dependencies [04bad47]
35
+ - Updated dependencies [e40b048]
36
+ - Updated dependencies [5a5b1bb]
37
+ - Updated dependencies [f1708c3]
38
+ - Updated dependencies [d254a81]
39
+ - Updated dependencies [7123c47]
40
+ - Updated dependencies [05befc1]
41
+ - Updated dependencies [0663b6a]
42
+ - Updated dependencies [f28f9b3]
43
+ - Updated dependencies [9f340f7]
44
+ - Updated dependencies [e53444f]
45
+ - Updated dependencies [5eedd26]
46
+ - Updated dependencies [92b0dd7]
47
+ - Updated dependencies [f919b6f]
48
+ - Updated dependencies [098e411]
49
+ - Updated dependencies [e1b7c01]
50
+ - Updated dependencies [9b94f87]
51
+ - Updated dependencies [a7df753]
52
+ - @zhin.js/agent@1.1.5
53
+ - @zhin.js/host-http@1.0.8
54
+ - @zhin.js/im-contract@1.0.3
55
+ - @zhin.js/adapter@1.1.7
56
+ - @zhin.js/plugin-runtime@1.1.5
57
+ - @zhin.js/command@1.0.9
58
+ - @zhin.js/core@1.5.4
59
+ - zhin.js@6.0.4
60
+
61
+ ## 5.0.4
62
+
63
+ ### Patch Changes
64
+
65
+ - f8c7a54: fix: im
66
+ - Updated dependencies [f8c7a54]
67
+ - @zhin.js/logger@1.0.76
68
+ - @zhin.js/host-http@1.0.7
69
+ - @zhin.js/adapter@1.1.6
70
+ - @zhin.js/agent@1.1.4
71
+ - @zhin.js/command@1.0.8
72
+ - @zhin.js/core@1.5.3
73
+ - @zhin.js/im-contract@1.0.2
74
+ - @zhin.js/plugin-runtime@1.1.4
75
+ - zhin.js@6.0.3
76
+
77
+ ## 5.0.3
78
+
79
+ ### Patch Changes
80
+
81
+ - Updated dependencies [afc0e66]
82
+ - Updated dependencies [2e41ad5]
83
+ - Updated dependencies [9f57124]
84
+ - @zhin.js/core@1.5.2
85
+ - @zhin.js/adapter@1.1.5
86
+ - @zhin.js/im-contract@1.0.1
87
+ - @zhin.js/plugin-runtime@1.1.3
88
+ - @zhin.js/command@1.0.7
89
+ - @zhin.js/host-http@1.0.6
90
+ - @zhin.js/agent@1.1.3
91
+ - zhin.js@6.0.2
92
+
3
93
  ## 5.0.2
4
94
 
5
95
  ### Patch Changes
package/README.md CHANGED
@@ -6,7 +6,7 @@ GitHub Plugin Runtime 适配器 — Issue/PR 评论区即聊天通道,GitHub A
6
6
 
7
7
  - **聊天通道**:Issue/PR 评论区映射为群聊,支持收发消息
8
8
  - **Webhook 入站**:HMAC-SHA256 验签 → `messageGatewayToken`
9
- - **出站**:`send({ target, payload })` → Issue/PR comment(target 为 channel ID)
9
+ - **出站**:`send({ conversation, payload })` → Issue/PR comment(`conversation.id` 为 channel ID)
10
10
  - **GitHub App 认证**:JWT → Installation Token
11
11
  - **Agent 工具**:`agent/` 下 star/bind/subscribe/workspace 等保留
12
12
 
@@ -85,7 +85,7 @@ plugins:
85
85
  | `src/gh-client.ts` | GitHub API 客户端 |
86
86
 
87
87
  - 入站:`httpHostToken` POST → `messageGatewayToken.receive`
88
- - 出站:`send({ target, payload })`
88
+ - 出站:`send({ conversation, payload })`
89
89
 
90
90
  ## License
91
91
 
@@ -30,8 +30,8 @@ export default defineAdapter({
30
30
  const config = resolveGithubConfig(context.config);
31
31
  const database = optionalDatabase(context);
32
32
  // 注册到插件运行时状态(github.endpoint list 的"运行中"数据源)
33
- context.use(githubRuntimeStateToken).endpoints.set(config.name, {
34
- name: config.name,
33
+ context.use(githubRuntimeStateToken).endpoints.set(config.id, {
34
+ id: config.id,
35
35
  mode: config.webhookSecret ? 'webhook' : 'api',
36
36
  });
37
37
  if (config.webhookSecret) {
@@ -7,7 +7,7 @@ import { messageGatewayToken } from '@zhin.js/core/runtime';
7
7
  import { httpHostToken } from '@zhin.js/host-http';
8
8
  import {
9
9
  databaseHostToken,
10
- type DatabaseHost,
10
+ type PluginDatabaseHost,
11
11
  } from '@zhin.js/plugin-runtime';
12
12
  import { GithubEndpoint } from '../src/endpoint.js';
13
13
  import { githubRuntimeStateToken } from '../src/github-runtime-state.js';
@@ -19,7 +19,7 @@ import {
19
19
  export { GithubEndpoint } from '../src/endpoint.js';
20
20
  export type { GithubEndpointOptions } from '../src/endpoint.js';
21
21
 
22
- function optionalDatabase(context: AdapterContext): DatabaseHost | undefined {
22
+ function optionalDatabase(context: AdapterContext): PluginDatabaseHost | undefined {
23
23
  try {
24
24
  return context.use(databaseHostToken);
25
25
  } catch {
@@ -38,8 +38,8 @@ export default defineAdapter<GithubAdapterConfig>({
38
38
  const config = resolveGithubConfig(context.config);
39
39
  const database = optionalDatabase(context);
40
40
  // 注册到插件运行时状态(github.endpoint list 的"运行中"数据源)
41
- context.use(githubRuntimeStateToken).endpoints.set(config.name, {
42
- name: config.name,
41
+ context.use(githubRuntimeStateToken).endpoints.set(config.id, {
42
+ id: config.id,
43
43
  mode: config.webhookSecret ? 'webhook' : 'api',
44
44
  });
45
45
  if (config.webhookSecret) {
package/lib/endpoint.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { EndpointInstance } from '@zhin.js/adapter';
1
+ import type { EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { HttpHost } from '@zhin.js/host-http';
4
- import type { CapabilityId, DatabaseHost } from '@zhin.js/plugin-runtime';
4
+ import type { CapabilityId, PluginDatabaseHost } from '@zhin.js/plugin-runtime';
5
5
  import { GhClient } from './gh-client.js';
6
6
  import { type GithubInboundComment, type ResolvedGithubConfig } from './protocol.js';
7
7
  import { WorkspaceManager } from './workspace-manager.js';
@@ -9,7 +9,7 @@ export interface GithubEndpointOptions {
9
9
  readonly id: CapabilityId;
10
10
  readonly gateway: MessageGateway;
11
11
  readonly http?: HttpHost;
12
- readonly database?: DatabaseHost;
12
+ readonly database?: PluginDatabaseHost;
13
13
  readonly config: ResolvedGithubConfig;
14
14
  readonly createClient?: (config: ResolvedGithubConfig) => GhClient;
15
15
  }
@@ -37,15 +37,12 @@ export declare class GithubEndpoint implements EndpointInstance {
37
37
  target_type: string;
38
38
  }[];
39
39
  getWorkspaceManager(): WorkspaceManager;
40
- getDatabase(): DatabaseHost | undefined;
40
+ getDatabase(): PluginDatabaseHost | undefined;
41
41
  start(): Promise<void>;
42
42
  open(): void;
43
43
  close(): void;
44
44
  stop(): Promise<void>;
45
- send({ target, payload }: {
46
- readonly target: string;
47
- readonly payload: unknown;
48
- }): Promise<string>;
45
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
49
46
  /** Test / internal: admit a parsed comment when open. */
50
47
  admit(comment: GithubInboundComment): void;
51
48
  }
package/lib/endpoint.js CHANGED
@@ -2,19 +2,19 @@
2
2
  * GithubEndpoint — lifecycle, outbound send, inbound admit.
3
3
  */
4
4
  import path from 'node:path';
5
- import { formatCompact, getLogger } from '@zhin.js/logger';
5
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
6
6
  import { GhClient } from './gh-client.js';
7
7
  import { registerGithubAgentEndpoint } from './github-agent-deps.js';
8
8
  import { lookupGithubOauthAccessToken } from './oauth-users.js';
9
- import { enrichInboundContent, formatInboundContent, formatOutboundBody, parseChannelId, } from './protocol.js';
9
+ import { enrichInboundContent, formatInboundContent, formatOutboundBody, githubInboundConversation, parseChannelId, } from './protocol.js';
10
10
  import { registerGithubWebhookRoutes } from './webhook.js';
11
11
  import { WorkspaceManager } from './workspace-manager.js';
12
- const logger = getLogger('github');
13
12
  /**
14
13
  * GitHub 是代码协作面(issue/PR),无好友/群/频道等 IM 社交概念,
15
14
  * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
16
15
  */
17
16
  export class GithubEndpoint {
17
+ #logger;
18
18
  #options;
19
19
  gh;
20
20
  config;
@@ -25,9 +25,10 @@ export class GithubEndpoint {
25
25
  #started = false;
26
26
  #unregisterAgent;
27
27
  constructor(options) {
28
+ this.#logger = getAdapterLogger('github', options.config.id);
28
29
  this.#options = options;
29
30
  this.config = options.config;
30
- this.name = options.config.name;
31
+ this.name = options.config.id;
31
32
  this.gh = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
32
33
  }
33
34
  getAPI() {
@@ -78,14 +79,14 @@ export class GithubEndpoint {
78
79
  throw new TypeError('GitHub webhook_secret requires httpHostToken');
79
80
  }
80
81
  this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
81
- logger.debug(formatCompact({
82
+ this.#logger.debug(formatCompact({
82
83
  endpoint: this.name,
83
84
  op: 'webhook',
84
85
  path: this.config.webhookPath,
85
86
  }));
86
87
  }
87
88
  else {
88
- logger.debug(formatCompact({
89
+ this.#logger.debug(formatCompact({
89
90
  endpoint: this.name,
90
91
  op: 'connect',
91
92
  mode: 'api-only',
@@ -95,7 +96,7 @@ export class GithubEndpoint {
95
96
  }
96
97
  catch (error) {
97
98
  await this.stop();
98
- logger.error('Failed to connect GitHub endpoint:', error);
99
+ this.#logger.error('Failed to connect GitHub endpoint:', error);
99
100
  throw error;
100
101
  }
101
102
  }
@@ -112,22 +113,22 @@ export class GithubEndpoint {
112
113
  this.#unregisterAgent?.();
113
114
  this.#unregisterAgent = undefined;
114
115
  this.#started = false;
115
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
116
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
116
117
  }
117
- async send({ target, payload }) {
118
- const parsed = parseChannelId(target);
118
+ async send({ conversation, payload }) {
119
+ const parsed = parseChannelId(conversation.id);
119
120
  if (!parsed)
120
- throw new Error(`无效的 GitHub channel ID: ${target}`);
121
+ throw new Error(`无效的 GitHub conversation ID: ${conversation.id}`);
121
122
  const text = formatOutboundBody(payload);
122
123
  const r = parsed.type === 'issue'
123
124
  ? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
124
125
  : await this.gh.createPRComment(parsed.repo, parsed.number, text);
125
126
  if (!r.ok)
126
127
  throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
127
- logger.debug(formatCompact({
128
+ this.#logger.debug(formatCompact({
128
129
  op: 'github_send',
129
130
  endpoint: this.name,
130
- target,
131
+ target: `${conversation.kind}:${conversation.id}`,
131
132
  messageId: r.data.id,
132
133
  }));
133
134
  return String(r.data.id);
@@ -139,23 +140,23 @@ export class GithubEndpoint {
139
140
  const botUser = this.config.botLogin || this.gh.getBotLogin() || this.gh.authenticatedUser;
140
141
  if (botUser && comment.sender === botUser)
141
142
  return;
143
+ const conversation = githubInboundConversation(String(this.#options.id), comment);
142
144
  const content = enrichInboundContent(formatInboundContent(comment.content), this.config, botUser ?? undefined, comment.repo);
143
145
  void this.#options.gateway.receive({
144
- adapter: this.#options.id,
145
- target: comment.channelId,
146
+ conversation,
147
+ message: { conversation, id: comment.id },
146
148
  content,
147
- sender: comment.sender,
148
- id: comment.id,
149
+ sender: { id: comment.sender, name: comment.sender },
150
+ endpointId: this.name,
149
151
  metadata: Object.freeze({
150
- endpoint: this.name,
151
152
  repo: comment.repo,
152
153
  kind: comment.kind,
153
154
  createdAt: comment.createdAt,
154
155
  }),
155
156
  }).catch((err) => {
156
- logger.warn(formatCompact({
157
+ this.#logger.warn(formatCompact({
157
158
  op: 'github_gateway_receive_failed',
158
- target: comment.channelId,
159
+ target: `${conversation.kind}:${conversation.id}`,
159
160
  error: err instanceof Error ? err.message : String(err),
160
161
  }));
161
162
  });
@@ -27,7 +27,7 @@ export interface GithubAgentEndpoint {
27
27
  getDatabase?(): unknown;
28
28
  }
29
29
  export interface GithubAgentDeps {
30
- getEndpoint: (endpointId?: string) => GithubAgentEndpoint;
30
+ getEndpoint: (endpointKey?: string) => GithubAgentEndpoint;
31
31
  /** Alias kept for existing agent handlers that call getAdapter(). */
32
32
  getAdapter: () => GithubAgentEndpoint;
33
33
  getWorkspaceManager: () => WorkspaceManager;
@@ -40,7 +40,7 @@ export interface GithubAgentDeps {
40
40
  error: (...args: unknown[]) => void;
41
41
  };
42
42
  }
43
- export declare function registerGithubAgentEndpoint(endpointId: string, endpoint: GithubAgentEndpoint): () => void;
43
+ export declare function registerGithubAgentEndpoint(endpointKey: string, endpoint: GithubAgentEndpoint): () => void;
44
44
  /** Optional override used by tests / transitional callers. Pass `null` to clear. */
45
45
  export declare function setGithubAgentDeps(deps: GithubAgentDeps | null): void;
46
46
  export declare function getGithubAgentDeps(): GithubAgentDeps;
@@ -4,11 +4,11 @@
4
4
  */
5
5
  const endpoints = new Map();
6
6
  let override = null;
7
- export function registerGithubAgentEndpoint(endpointId, endpoint) {
8
- endpoints.set(endpointId, endpoint);
7
+ export function registerGithubAgentEndpoint(endpointKey, endpoint) {
8
+ endpoints.set(endpointKey, endpoint);
9
9
  return () => {
10
- if (endpoints.get(endpointId) === endpoint) {
11
- endpoints.delete(endpointId);
10
+ if (endpoints.get(endpointKey) === endpoint) {
11
+ endpoints.delete(endpointKey);
12
12
  }
13
13
  };
14
14
  }
@@ -16,11 +16,11 @@ export function registerGithubAgentEndpoint(endpointId, endpoint) {
16
16
  export function setGithubAgentDeps(deps) {
17
17
  override = deps;
18
18
  }
19
- function lookup(endpointId) {
20
- if (endpointId) {
21
- const registered = endpoints.get(endpointId);
19
+ function lookup(endpointKey) {
20
+ if (endpointKey) {
21
+ const registered = endpoints.get(endpointKey);
22
22
  if (!registered)
23
- throw new Error(`Endpoint ${endpointId} 不存在`);
23
+ throw new Error(`Endpoint ${endpointKey} 不存在`);
24
24
  return registered;
25
25
  }
26
26
  const first = endpoints.values().next().value;
@@ -1,4 +1,4 @@
1
- import type { DatabaseHost } from '@zhin.js/plugin-runtime';
1
+ import type { DatabaseHost, PluginDatabaseHost } from '@zhin.js/plugin-runtime';
2
2
  export declare const GITHUB_OAUTH_USERS_TABLE = "github_oauth_users";
3
3
  export declare const GITHUB_OAUTH_USERS_SCHEMA: {
4
4
  readonly id: {
@@ -30,4 +30,4 @@ export declare function defineGithubOauthUsersTable(host: {
30
30
  define(name: string, definition: Record<string, unknown>): void;
31
31
  }): void;
32
32
  /** Resolve a stored PAT/device-flow token; null when DB missing or no row. */
33
- export declare function lookupGithubOauthAccessToken(database: DatabaseHost | undefined, platform: string, platformUid: string): Promise<string | null>;
33
+ export declare function lookupGithubOauthAccessToken(database: DatabaseHost | PluginDatabaseHost | undefined, platform: string, platformUid: string): Promise<string | null>;
package/lib/protocol.d.ts CHANGED
@@ -3,12 +3,13 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  */
5
5
  import type { IncomingMessage } from 'node:http';
6
+ import type { ConversationRef } from '@zhin.js/im-contract';
6
7
  import { type GenericWebhookPayload, type IssueCommentPayload, type PRReviewCommentPayload, type PRReviewPayload } from './types.js';
7
8
  export type { EventType, GenericWebhookPayload, GitHubComment, GitHubEndpointConfig, GitHubIssue, GitHubOAuthUser, GitHubPR, GitHubRepo, GitHubUser, IssueCommentPayload, ParsedChannel, PRReviewCommentPayload, PRReviewPayload, Subscription, } from './types.js';
8
9
  export { buildChannelId, parseChannelId } from './types.js';
9
10
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
10
11
  export interface GithubAdapterConfig {
11
- readonly name?: string;
12
+ readonly id?: string;
12
13
  readonly host?: string;
13
14
  readonly app_id?: string | number;
14
15
  readonly appId?: string | number;
@@ -41,7 +42,7 @@ export interface GithubAdapterConfig {
41
42
  }
42
43
  export interface ResolvedGithubConfig {
43
44
  readonly context: 'github';
44
- readonly name: string;
45
+ readonly id: string;
45
46
  readonly host?: string;
46
47
  readonly appId?: string | number;
47
48
  readonly privateKey?: string;
@@ -65,6 +66,12 @@ export interface GithubInboundComment {
65
66
  readonly kind: 'issue_comment' | 'pr_review_comment' | 'pr_review';
66
67
  readonly createdAt: number;
67
68
  }
69
+ /**
70
+ * 入站归一化 → ConversationRef:Issue/PR 评论区即仓库容器内的 channel 会话,
71
+ * 仓库(owner/repo)进 `parent`。出站侧用 `parseChannelId(conversation.id)`
72
+ * 在平台边界还原 repo/type/number。
73
+ */
74
+ export declare function githubInboundConversation(endpointKey: string, comment: GithubInboundComment): ConversationRef;
68
75
  export declare function resolveGithubConfig(config?: GithubAdapterConfig): ResolvedGithubConfig;
69
76
  export declare function normalizeWebhookPath(path: string): string;
70
77
  export declare function shouldAutoReplyRepo(config: Pick<ResolvedGithubConfig, 'autoReplyRepos'>, repo: string): boolean;
package/lib/protocol.js CHANGED
@@ -9,6 +9,19 @@ import { formatCompact, getLogger } from '@zhin.js/logger';
9
9
  const logger = getLogger('github');
10
10
  import { buildChannelId, } from './types.js';
11
11
  export { buildChannelId, parseChannelId } from './types.js';
12
+ /**
13
+ * 入站归一化 → ConversationRef:Issue/PR 评论区即仓库容器内的 channel 会话,
14
+ * 仓库(owner/repo)进 `parent`。出站侧用 `parseChannelId(conversation.id)`
15
+ * 在平台边界还原 repo/type/number。
16
+ */
17
+ export function githubInboundConversation(endpointKey, comment) {
18
+ return {
19
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
20
+ kind: 'channel',
21
+ id: comment.channelId,
22
+ parent: { kind: 'channel', id: comment.repo },
23
+ };
24
+ }
12
25
  export function resolveGithubConfig(config = {}) {
13
26
  const entry = config.endpoints?.find((item) => item.context === 'github');
14
27
  const appIdRaw = pickCredential(config.appId != null ? String(config.appId) : undefined, config.app_id != null ? String(config.app_id) : undefined, entry?.appId != null ? String(entry.appId) : undefined, entry?.app_id != null ? String(entry.app_id) : undefined, process.env.GITHUB_APP_ID);
@@ -17,8 +30,8 @@ export function resolveGithubConfig(config = {}) {
17
30
  if (!appId || !privateKey) {
18
31
  throw new TypeError('GitHub adapter requires app_id + private_key (plugins.<key> or GITHUB_APP_ID)');
19
32
  }
20
- const name = (typeof config.name === 'string' && config.name)
21
- || (typeof entry?.name === 'string' && entry.name)
33
+ const id = (typeof config.id === 'string' && config.id)
34
+ || (typeof entry?.id === 'string' && entry.id)
22
35
  || process.env.GITHUB_BOT_NAME
23
36
  || 'github-bot';
24
37
  const webhookSecret = config.webhookSecret ?? config.webhook_secret
@@ -38,7 +51,7 @@ export function resolveGithubConfig(config = {}) {
38
51
  ?? entry?.workspaceRoot ?? entry?.workspace_root;
39
52
  return {
40
53
  context: 'github',
41
- name,
54
+ id,
42
55
  ...(host ? { host } : {}),
43
56
  ...(appId != null ? { appId } : {}),
44
57
  ...(privateKey ? { privateKey } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-github",
3
- "version": "5.0.2",
3
+ "version": "5.0.5",
4
4
  "description": "Zhin.js GitHub adapter for Plugin Runtime (App auth + webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -41,21 +41,22 @@
41
41
  "directory": "plugins/adapters/github"
42
42
  },
43
43
  "dependencies": {
44
- "@zhin.js/adapter": "1.1.4",
45
- "@zhin.js/command": "1.0.6",
46
- "@zhin.js/core": "1.5.1",
47
- "@zhin.js/host-http": "1.0.5",
48
- "@zhin.js/logger": "1.0.75",
49
- "@zhin.js/plugin-runtime": "1.1.2"
44
+ "@zhin.js/adapter": "1.1.7",
45
+ "@zhin.js/command": "1.0.9",
46
+ "@zhin.js/core": "1.5.4",
47
+ "@zhin.js/host-http": "1.0.8",
48
+ "@zhin.js/im-contract": "1.0.3",
49
+ "@zhin.js/logger": "1.0.76",
50
+ "@zhin.js/plugin-runtime": "1.1.5"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "zod": "^4.0.0",
53
- "@zhin.js/adapter": "1.1.4",
54
- "@zhin.js/agent": "1.1.2",
55
- "@zhin.js/core": "1.5.1",
56
- "@zhin.js/host-http": "1.0.5",
57
- "@zhin.js/plugin-runtime": "1.1.2",
58
- "zhin.js": "6.0.1"
54
+ "@zhin.js/adapter": "1.1.7",
55
+ "@zhin.js/agent": "1.1.5",
56
+ "@zhin.js/core": "1.5.4",
57
+ "@zhin.js/host-http": "1.0.8",
58
+ "@zhin.js/plugin-runtime": "1.1.5",
59
+ "zhin.js": "6.0.4"
59
60
  },
60
61
  "peerDependenciesMeta": {
61
62
  "zhin.js": {
@@ -73,8 +74,8 @@
73
74
  "typescript": "^6.0.3",
74
75
  "vitest": "^4.1.10",
75
76
  "zod": "^4.4.3",
76
- "@zhin.js/agent": "1.1.2",
77
- "zhin.js": "6.0.1"
77
+ "@zhin.js/agent": "1.1.5",
78
+ "zhin.js": "6.0.4"
78
79
  },
79
80
  "files": [
80
81
  "adapters",
package/schema.json CHANGED
@@ -35,6 +35,24 @@
35
35
  "type": "string",
36
36
  "description": "Managed git workspace root"
37
37
  },
38
+ "master": {
39
+ "type": [
40
+ "string",
41
+ "number"
42
+ ],
43
+ "description": "框架 master(GitHub user login or id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
44
+ },
45
+ "trusted": {
46
+ "type": "array",
47
+ "items": {
48
+ "type": [
49
+ "string",
50
+ "number"
51
+ ],
52
+ "description": "Trusted GitHub user login or id"
53
+ },
54
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
55
+ },
38
56
  "endpoints": {
39
57
  "type": "array",
40
58
  "description": "多账号:一个插件实例挂多个 endpoint",
@@ -42,9 +60,23 @@
42
60
  "type": "object",
43
61
  "additionalProperties": true,
44
62
  "properties": {
45
- "name": {
46
- "type": "string",
47
- "description": "GitHub App bot name"
63
+ "master": {
64
+ "type": [
65
+ "string",
66
+ "number"
67
+ ],
68
+ "description": "本 endpoint 的框架 master(GitHub user login or id);覆盖顶层 master"
69
+ },
70
+ "trusted": {
71
+ "type": "array",
72
+ "items": {
73
+ "type": [
74
+ "string",
75
+ "number"
76
+ ],
77
+ "description": "Trusted GitHub user login or id"
78
+ },
79
+ "description": "本 endpoint 的 trusted 列表"
48
80
  },
49
81
  "app_id": {
50
82
  "type": [
@@ -75,10 +107,14 @@
75
107
  "webhookSecret": {
76
108
  "type": "string",
77
109
  "description": "Webhook HMAC secret (camelCase alias)"
110
+ },
111
+ "id": {
112
+ "type": "string",
113
+ "description": "GitHub App bot name"
78
114
  }
79
115
  },
80
116
  "required": [
81
- "name"
117
+ "id"
82
118
  ]
83
119
  }
84
120
  },
package/src/endpoint.ts CHANGED
@@ -2,11 +2,11 @@
2
2
  * GithubEndpoint — lifecycle, outbound send, inbound admit.
3
3
  */
4
4
  import path from 'node:path';
5
- import type { EndpointInstance } from '@zhin.js/adapter';
5
+ import type { EndpointInstance, EndpointSendRequest } from '@zhin.js/adapter';
6
6
  import type { MessageGateway } from '@zhin.js/core/runtime';
7
7
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
8
- import { formatCompact, getLogger } from '@zhin.js/logger';
9
- import type { CapabilityId, DatabaseHost } from '@zhin.js/plugin-runtime';
8
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
9
+ import type { CapabilityId, PluginDatabaseHost } from '@zhin.js/plugin-runtime';
10
10
  import { GhClient } from './gh-client.js';
11
11
  import { registerGithubAgentEndpoint } from './github-agent-deps.js';
12
12
  import { lookupGithubOauthAccessToken } from './oauth-users.js';
@@ -14,6 +14,7 @@ import {
14
14
  enrichInboundContent,
15
15
  formatInboundContent,
16
16
  formatOutboundBody,
17
+ githubInboundConversation,
17
18
  parseChannelId,
18
19
  type GithubInboundComment,
19
20
  type ResolvedGithubConfig,
@@ -21,13 +22,11 @@ import {
21
22
  import { registerGithubWebhookRoutes } from './webhook.js';
22
23
  import { WorkspaceManager } from './workspace-manager.js';
23
24
 
24
- const logger = getLogger('github');
25
-
26
25
  export interface GithubEndpointOptions {
27
26
  readonly id: CapabilityId;
28
27
  readonly gateway: MessageGateway;
29
28
  readonly http?: HttpHost;
30
- readonly database?: DatabaseHost;
29
+ readonly database?: PluginDatabaseHost;
31
30
  readonly config: ResolvedGithubConfig;
32
31
  readonly createClient?: (config: ResolvedGithubConfig) => GhClient;
33
32
  }
@@ -37,6 +36,8 @@ export interface GithubEndpointOptions {
37
36
  * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
38
37
  */
39
38
  export class GithubEndpoint implements EndpointInstance {
39
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
40
+
40
41
  readonly #options: GithubEndpointOptions;
41
42
  readonly gh: GhClient;
42
43
  readonly config: ResolvedGithubConfig;
@@ -48,9 +49,10 @@ export class GithubEndpoint implements EndpointInstance {
48
49
  #unregisterAgent?: () => void;
49
50
 
50
51
  constructor(options: GithubEndpointOptions) {
52
+ this.#logger = getAdapterLogger('github', options.config.id);
51
53
  this.#options = options;
52
54
  this.config = options.config;
53
- this.name = options.config.name;
55
+ this.name = options.config.id;
54
56
  this.gh = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
55
57
  }
56
58
 
@@ -94,7 +96,7 @@ export class GithubEndpoint implements EndpointInstance {
94
96
  return this.#workspaceManager;
95
97
  }
96
98
 
97
- getDatabase(): DatabaseHost | undefined {
99
+ getDatabase(): PluginDatabaseHost | undefined {
98
100
  return this.#options.database;
99
101
  }
100
102
 
@@ -110,13 +112,13 @@ export class GithubEndpoint implements EndpointInstance {
110
112
  throw new TypeError('GitHub webhook_secret requires httpHostToken');
111
113
  }
112
114
  this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
113
- logger.debug(formatCompact({
115
+ this.#logger.debug(formatCompact({
114
116
  endpoint: this.name,
115
117
  op: 'webhook',
116
118
  path: this.config.webhookPath,
117
119
  }));
118
120
  } else {
119
- logger.debug(formatCompact({
121
+ this.#logger.debug(formatCompact({
120
122
  endpoint: this.name,
121
123
  op: 'connect',
122
124
  mode: 'api-only',
@@ -125,7 +127,7 @@ export class GithubEndpoint implements EndpointInstance {
125
127
  }
126
128
  } catch (error) {
127
129
  await this.stop();
128
- logger.error('Failed to connect GitHub endpoint:', error);
130
+ this.#logger.error('Failed to connect GitHub endpoint:', error);
129
131
  throw error;
130
132
  }
131
133
  }
@@ -144,21 +146,21 @@ export class GithubEndpoint implements EndpointInstance {
144
146
  this.#unregisterAgent?.();
145
147
  this.#unregisterAgent = undefined;
146
148
  this.#started = false;
147
- logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
149
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
148
150
  }
149
151
 
150
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
151
- const parsed = parseChannelId(target);
152
- if (!parsed) throw new Error(`无效的 GitHub channel ID: ${target}`);
152
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
153
+ const parsed = parseChannelId(conversation.id);
154
+ if (!parsed) throw new Error(`无效的 GitHub conversation ID: ${conversation.id}`);
153
155
  const text = formatOutboundBody(payload);
154
156
  const r = parsed.type === 'issue'
155
157
  ? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
156
158
  : await this.gh.createPRComment(parsed.repo, parsed.number, text);
157
159
  if (!r.ok) throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
158
- logger.debug(formatCompact({
160
+ this.#logger.debug(formatCompact({
159
161
  op: 'github_send',
160
162
  endpoint: this.name,
161
- target,
163
+ target: `${conversation.kind}:${conversation.id}`,
162
164
  messageId: r.data.id,
163
165
  }));
164
166
  return String(r.data.id);
@@ -169,6 +171,7 @@ export class GithubEndpoint implements EndpointInstance {
169
171
  if (!this.#open) return;
170
172
  const botUser = this.config.botLogin || this.gh.getBotLogin() || this.gh.authenticatedUser;
171
173
  if (botUser && comment.sender === botUser) return;
174
+ const conversation = githubInboundConversation(String(this.#options.id), comment);
172
175
  const content = enrichInboundContent(
173
176
  formatInboundContent(comment.content),
174
177
  this.config,
@@ -176,21 +179,20 @@ export class GithubEndpoint implements EndpointInstance {
176
179
  comment.repo,
177
180
  );
178
181
  void this.#options.gateway.receive({
179
- adapter: this.#options.id,
180
- target: comment.channelId,
182
+ conversation,
183
+ message: { conversation, id: comment.id },
181
184
  content,
182
- sender: comment.sender,
183
- id: comment.id,
185
+ sender: { id: comment.sender, name: comment.sender },
186
+ endpointId: this.name,
184
187
  metadata: Object.freeze({
185
- endpoint: this.name,
186
188
  repo: comment.repo,
187
189
  kind: comment.kind,
188
190
  createdAt: comment.createdAt,
189
191
  }),
190
192
  }).catch((err) => {
191
- logger.warn(formatCompact({
193
+ this.#logger.warn(formatCompact({
192
194
  op: 'github_gateway_receive_failed',
193
- target: comment.channelId,
195
+ target: `${conversation.kind}:${conversation.id}`,
194
196
  error: err instanceof Error ? err.message : String(err),
195
197
  }));
196
198
  });
@@ -23,7 +23,7 @@ export interface GithubAgentEndpoint {
23
23
  }
24
24
 
25
25
  export interface GithubAgentDeps {
26
- getEndpoint: (endpointId?: string) => GithubAgentEndpoint;
26
+ getEndpoint: (endpointKey?: string) => GithubAgentEndpoint;
27
27
  /** Alias kept for existing agent handlers that call getAdapter(). */
28
28
  getAdapter: () => GithubAgentEndpoint;
29
29
  getWorkspaceManager: () => WorkspaceManager;
@@ -39,13 +39,13 @@ const endpoints = new Map<string, GithubAgentEndpoint>();
39
39
  let override: GithubAgentDeps | null = null;
40
40
 
41
41
  export function registerGithubAgentEndpoint(
42
- endpointId: string,
42
+ endpointKey: string,
43
43
  endpoint: GithubAgentEndpoint,
44
44
  ): () => void {
45
- endpoints.set(endpointId, endpoint);
45
+ endpoints.set(endpointKey, endpoint);
46
46
  return () => {
47
- if (endpoints.get(endpointId) === endpoint) {
48
- endpoints.delete(endpointId);
47
+ if (endpoints.get(endpointKey) === endpoint) {
48
+ endpoints.delete(endpointKey);
49
49
  }
50
50
  };
51
51
  }
@@ -55,10 +55,10 @@ export function setGithubAgentDeps(deps: GithubAgentDeps | null): void {
55
55
  override = deps;
56
56
  }
57
57
 
58
- function lookup(endpointId?: string): GithubAgentEndpoint {
59
- if (endpointId) {
60
- const registered = endpoints.get(endpointId);
61
- if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
58
+ function lookup(endpointKey?: string): GithubAgentEndpoint {
59
+ if (endpointKey) {
60
+ const registered = endpoints.get(endpointKey);
61
+ if (!registered) throw new Error(`Endpoint ${endpointKey} 不存在`);
62
62
  return registered;
63
63
  }
64
64
  const first = endpoints.values().next().value;
@@ -2,7 +2,7 @@
2
2
  * github_oauth_users — SSOT for Runtime user-token binding (gh bind / Endpoint lookup).
3
3
  */
4
4
  import { formatCompact, getLogger } from '@zhin.js/logger';
5
- import type { DatabaseHost } from '@zhin.js/plugin-runtime';
5
+ import type { DatabaseHost, PluginDatabaseHost } from '@zhin.js/plugin-runtime';
6
6
 
7
7
  const logger = getLogger('github');
8
8
 
@@ -25,7 +25,7 @@ export function defineGithubOauthUsersTable(
25
25
 
26
26
  /** Resolve a stored PAT/device-flow token; null when DB missing or no row. */
27
27
  export async function lookupGithubOauthAccessToken(
28
- database: DatabaseHost | undefined,
28
+ database: DatabaseHost | PluginDatabaseHost | undefined,
29
29
  platform: string,
30
30
  platformUid: string,
31
31
  ): Promise<string | null> {
package/src/protocol.ts CHANGED
@@ -7,6 +7,7 @@ import { createHmac, timingSafeEqual } from 'node:crypto';
7
7
  import type { IncomingMessage } from 'node:http';
8
8
  import { pickCredential } from '@zhin.js/adapter';
9
9
  import { isMediaRef } from '@zhin.js/core';
10
+ import type { ConversationRef } from '@zhin.js/im-contract';
10
11
  import { formatCompact, getLogger } from '@zhin.js/logger';
11
12
 
12
13
  const logger = getLogger('github');
@@ -39,7 +40,7 @@ export { buildChannelId, parseChannelId } from './types.js';
39
40
 
40
41
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
41
42
  export interface GithubAdapterConfig {
42
- readonly name?: string;
43
+ readonly id?: string;
43
44
  readonly host?: string;
44
45
  readonly app_id?: string | number;
45
46
  readonly appId?: string | number;
@@ -73,7 +74,7 @@ export interface GithubAdapterConfig {
73
74
 
74
75
  export interface ResolvedGithubConfig {
75
76
  readonly context: 'github';
76
- readonly name: string;
77
+ readonly id: string;
77
78
  readonly host?: string;
78
79
  readonly appId?: string | number;
79
80
  readonly privateKey?: string;
@@ -100,6 +101,23 @@ export interface GithubInboundComment {
100
101
  readonly createdAt: number;
101
102
  }
102
103
 
104
+ /**
105
+ * 入站归一化 → ConversationRef:Issue/PR 评论区即仓库容器内的 channel 会话,
106
+ * 仓库(owner/repo)进 `parent`。出站侧用 `parseChannelId(conversation.id)`
107
+ * 在平台边界还原 repo/type/number。
108
+ */
109
+ export function githubInboundConversation(
110
+ endpointKey: string,
111
+ comment: GithubInboundComment,
112
+ ): ConversationRef {
113
+ return {
114
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
115
+ kind: 'channel',
116
+ id: comment.channelId,
117
+ parent: { kind: 'channel', id: comment.repo },
118
+ };
119
+ }
120
+
103
121
  export function resolveGithubConfig(config: GithubAdapterConfig = {}): ResolvedGithubConfig {
104
122
  const entry = config.endpoints?.find((item) => item.context === 'github');
105
123
  const appIdRaw = pickCredential(
@@ -121,8 +139,8 @@ export function resolveGithubConfig(config: GithubAdapterConfig = {}): ResolvedG
121
139
  'GitHub adapter requires app_id + private_key (plugins.<key> or GITHUB_APP_ID)',
122
140
  );
123
141
  }
124
- const name = (typeof config.name === 'string' && config.name)
125
- || (typeof entry?.name === 'string' && entry.name)
142
+ const id = (typeof config.id === 'string' && config.id)
143
+ || (typeof entry?.id === 'string' && entry.id)
126
144
  || process.env.GITHUB_BOT_NAME
127
145
  || 'github-bot';
128
146
  const webhookSecret = config.webhookSecret ?? config.webhook_secret
@@ -147,7 +165,7 @@ export function resolveGithubConfig(config: GithubAdapterConfig = {}): ResolvedG
147
165
 
148
166
  return {
149
167
  context: 'github',
150
- name,
168
+ id,
151
169
  ...(host ? { host } : {}),
152
170
  ...(appId != null ? { appId } : {}),
153
171
  ...(privateKey ? { privateKey } : {}),