@turingflow/agent-kit 0.0.0-stage → 0.1.1

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 (93) hide show
  1. package/README.md +592 -2
  2. package/dist/agent.d.ts +27 -0
  3. package/dist/agent.js +31 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +105 -0
  6. package/dist/handlers.d.ts +92 -0
  7. package/dist/handlers.js +211 -0
  8. package/dist/index.d.ts +13 -0
  9. package/dist/index.js +10 -0
  10. package/dist/sdk.d.ts +106 -0
  11. package/dist/sdk.js +309 -0
  12. package/dist/server/agent.d.ts +27 -0
  13. package/dist/server/agent.js +31 -0
  14. package/dist/server/artifact-hooks.d.ts +6 -0
  15. package/dist/server/artifact-hooks.js +37 -0
  16. package/dist/server/artifact-tool-tracker.d.ts +24 -0
  17. package/dist/server/artifact-tool-tracker.js +347 -0
  18. package/dist/server/artifacts.d.ts +227 -0
  19. package/dist/server/artifacts.js +1061 -0
  20. package/dist/server/auth/config.d.ts +34 -0
  21. package/dist/server/auth/config.js +82 -0
  22. package/dist/server/auth/db.d.ts +159 -0
  23. package/dist/server/auth/db.js +680 -0
  24. package/dist/server/auth/handlers.d.ts +27 -0
  25. package/dist/server/auth/handlers.js +297 -0
  26. package/dist/server/auth/index.d.ts +15 -0
  27. package/dist/server/auth/index.js +14 -0
  28. package/dist/server/auth/login-options.d.ts +14 -0
  29. package/dist/server/auth/login-options.js +50 -0
  30. package/dist/server/auth/providers/dingtalk.d.ts +16 -0
  31. package/dist/server/auth/providers/dingtalk.js +177 -0
  32. package/dist/server/auth/providers/feishu.d.ts +15 -0
  33. package/dist/server/auth/providers/feishu.js +100 -0
  34. package/dist/server/auth/providers/index.d.ts +13 -0
  35. package/dist/server/auth/providers/index.js +24 -0
  36. package/dist/server/auth/session.d.ts +48 -0
  37. package/dist/server/auth/session.js +108 -0
  38. package/dist/server/auth/types.d.ts +118 -0
  39. package/dist/server/auth/types.js +7 -0
  40. package/dist/server/browser-agent-mcp.d.ts +67 -0
  41. package/dist/server/browser-agent-mcp.js +608 -0
  42. package/dist/server/cli.d.ts +6 -0
  43. package/dist/server/cli.js +112 -0
  44. package/dist/server/debug-log.d.ts +25 -0
  45. package/dist/server/debug-log.js +86 -0
  46. package/dist/server/handlers.d.ts +141 -0
  47. package/dist/server/handlers.js +523 -0
  48. package/dist/server/index.d.ts +30 -0
  49. package/dist/server/index.js +28 -0
  50. package/dist/server/kit-api.d.ts +23 -0
  51. package/dist/server/kit-api.js +153 -0
  52. package/dist/server/model-sources.d.ts +32 -0
  53. package/dist/server/model-sources.js +58 -0
  54. package/dist/server/oa.d.ts +87 -0
  55. package/dist/server/oa.js +397 -0
  56. package/dist/server/providers/openrouter.d.ts +51 -0
  57. package/dist/server/providers/openrouter.js +283 -0
  58. package/dist/server/render-markdown.d.ts +2 -0
  59. package/dist/server/render-markdown.js +50 -0
  60. package/dist/server/sdk.d.ts +269 -0
  61. package/dist/server/sdk.js +2025 -0
  62. package/dist/server/session-access.d.ts +5 -0
  63. package/dist/server/session-access.js +68 -0
  64. package/dist/server/session-fork.d.ts +10 -0
  65. package/dist/server/session-fork.js +142 -0
  66. package/dist/server/session-store.d.ts +56 -0
  67. package/dist/server/session-store.js +148 -0
  68. package/dist/server/sessions.d.ts +24 -0
  69. package/dist/server/sessions.js +541 -0
  70. package/dist/server/share.d.ts +15 -0
  71. package/dist/server/share.js +433 -0
  72. package/dist/server/tool-activity.d.ts +2 -0
  73. package/dist/server/tool-activity.js +26 -0
  74. package/dist/server/transcription/xiaomi-mimo.d.ts +8 -0
  75. package/dist/server/transcription/xiaomi-mimo.js +86 -0
  76. package/dist/server/upload.d.ts +13 -0
  77. package/dist/server/upload.js +81 -0
  78. package/dist/server/user-files.d.ts +28 -0
  79. package/dist/server/user-files.js +311 -0
  80. package/dist/server/voice.d.ts +25 -0
  81. package/dist/server/voice.js +61 -0
  82. package/dist/server/web-fetch.d.ts +71 -0
  83. package/dist/server/web-fetch.js +330 -0
  84. package/dist/styles.css +3354 -0
  85. package/dist/transcription/xiaomi-mimo.d.ts +8 -0
  86. package/dist/transcription/xiaomi-mimo.js +86 -0
  87. package/dist/ui/index.d.ts +358 -0
  88. package/dist/ui/index.js +7730 -0
  89. package/dist/upload.d.ts +13 -0
  90. package/dist/upload.js +81 -0
  91. package/dist/voice.d.ts +25 -0
  92. package/dist/voice.js +61 -0
  93. package/package.json +74 -4
@@ -0,0 +1,27 @@
1
+ import type { AuthConfig, SessionUser } from "./types.js";
2
+ /** 校验请求中的会话 Cookie,返回 SessionUser 或 null。 */
3
+ export declare function verifySession(request: Request, config: AuthConfig): SessionUser | null;
4
+ /** 便捷方法:自动读取认证配置并校验会话。 */
5
+ export declare function getSessionUser(request: Request): SessionUser | null;
6
+ export interface LoginHandlers {
7
+ login: {
8
+ GET: (request: Request) => Promise<Response> | Response;
9
+ };
10
+ callback: {
11
+ GET: (request: Request) => Promise<Response> | Response;
12
+ };
13
+ sso: {
14
+ POST: (request: Request) => Promise<Response> | Response;
15
+ };
16
+ session: {
17
+ GET: (request: Request) => Promise<Response> | Response;
18
+ };
19
+ logout: {
20
+ POST: (request: Request) => Promise<Response> | Response;
21
+ };
22
+ }
23
+ export interface LoginHandlersOptions {
24
+ /** 覆盖从 env 读取的认证配置(测试注入用) */
25
+ config?: AuthConfig;
26
+ }
27
+ export declare function createLoginHandlers(options?: LoginHandlersOptions): LoginHandlers;
@@ -0,0 +1,297 @@
1
+ /**
2
+ * agent-kit · 登录路由工厂(Next.js App Router)
3
+ *
4
+ * 第三方 Next.js 工程一行引入即可获得完整登录能力,无需重写 OAuth / 回调 / 免登 / 会话 /
5
+ * 登出逻辑:
6
+ *
7
+ * // app/api/auth/login/route.js
8
+ * import { createLoginHandlers } from "agent-kit";
9
+ * export const runtime = "nodejs";
10
+ * export const { GET } = createLoginHandlers().login;
11
+ *
12
+ * // app/api/auth/callback/route.js -> .callback
13
+ * // app/api/auth/sso/route.js -> .sso
14
+ * // app/api/auth/session/route.js -> .session
15
+ * // app/api/auth/logout/route.js -> .logout
16
+ *
17
+ * 设计要点:
18
+ * - 采用标准 Web Response + 手写 Set-Cookie,不依赖 next/server,保证 agent-kit 自包含、可复用;
19
+ * - 仅暴露 AuthProvider 抽象与归一化 AuthUser,不感知钉钉/飞书差异;
20
+ * - 数据库落库(users 表 upsert)在已配置 Postgres 时生效。
21
+ */
22
+ import { loadAuthConfig } from "./config.js";
23
+ import { createSessionToken, verifySessionToken, toSessionUser, generateState, sessionCookieOptions, stateCookieOptions, } from "./session.js";
24
+ import { createProvider, createProviderFor } from "./providers/index.js";
25
+ import { getLoginOptions } from "./login-options.js";
26
+ import { upsertUser, touchLastLogin, isDbEnabled } from "./db.js";
27
+ const SESSION_COOKIE = "agent_session";
28
+ const STATE_COOKIE = "agent_oauth_state";
29
+ const NEXT_COOKIE = "agent_login_next";
30
+ // ----------------------------- Cookie 工具(手写,避免引入 next/server) -----------------------------
31
+ function getCookie(request, name) {
32
+ const header = request.headers.get("cookie");
33
+ if (!header)
34
+ return undefined;
35
+ for (const part of header.split(";")) {
36
+ const idx = part.indexOf("=");
37
+ if (idx < 0)
38
+ continue;
39
+ const key = part.slice(0, idx).trim();
40
+ if (key === name)
41
+ return decodeURIComponent(part.slice(idx + 1).trim());
42
+ }
43
+ return undefined;
44
+ }
45
+ function setCookie(headers, name, value, opts) {
46
+ const parts = [
47
+ `${name}=${encodeURIComponent(value)}`,
48
+ `Path=${opts.path}`,
49
+ `Max-Age=${opts.maxAge}`,
50
+ "HttpOnly",
51
+ "SameSite=Lax",
52
+ ];
53
+ if (opts.secure)
54
+ parts.push("Secure");
55
+ headers.append("Set-Cookie", parts.join("; "));
56
+ }
57
+ function clearCookie(headers, name, path, secure = false) {
58
+ const parts = [`${name}=`, `Path=${path}`, "Max-Age=0", "HttpOnly"];
59
+ // 必须与下发时一致(secure 时 SameSite=None 才能匹配清除 Secure Cookie)
60
+ parts.push(secure ? "SameSite=None" : "SameSite=Lax");
61
+ if (secure)
62
+ parts.push("Secure");
63
+ headers.append("Set-Cookie", parts.join("; "));
64
+ }
65
+ /** 解析对外 origin:优先 env(APP_PUBLIC_URL),否则用请求头(x-forwarded-host / host)推断。
66
+ * 注意:不能依赖 `new URL(request.url).origin`——Next.js dev 模式下 request.url 的 host 固定为
67
+ * localhost(忽略实际 Host header),会导致回调地址与真实访问地址不一致。 */
68
+ function publicOrigin(request, config) {
69
+ if (config.publicUrl)
70
+ return config.publicUrl;
71
+ const proto = request.headers.get("x-forwarded-proto") || "http";
72
+ const host = request.headers.get("x-forwarded-host") || request.headers.get("host");
73
+ if (host)
74
+ return `${proto}://${host}`;
75
+ try {
76
+ return new URL(request.url).origin;
77
+ }
78
+ catch {
79
+ return `${proto}://localhost`;
80
+ }
81
+ }
82
+ function redirect(url) {
83
+ return new Response(null, { status: 302, headers: { Location: url } });
84
+ }
85
+ function safeNext(value, config) {
86
+ // 防止开放重定向:仅允许跳回本站 basePath 下的路径
87
+ const prefix = config.cookiePath; // 通常等于 basePath
88
+ // 兜底必须去尾斜杠再拼,避免 cookiePath="/" 时拼出 "//"(protocol-relative URL,
89
+ // new URL("//", base) 抛 Invalid URL,导致登录成功后回跳失败)
90
+ const fallback = `${prefix.replace(/\/+$/, "")}/`;
91
+ return value && value.startsWith(prefix) ? value : fallback;
92
+ }
93
+ function loginError(request, config, reason) {
94
+ const url = new URL(`${publicOrigin(request, config)}${config.cookiePath.replace(/\/+$/, "")}/login`);
95
+ url.searchParams.set("error", reason);
96
+ return redirect(url.toString());
97
+ }
98
+ // ----------------------------- 会话读取 -----------------------------
99
+ /** 校验请求中的会话 Cookie,返回 SessionUser 或 null。 */
100
+ export function verifySession(request, config) {
101
+ return verifySessionToken(getCookie(request, SESSION_COOKIE), config.sessionSecret);
102
+ }
103
+ /** 便捷方法:自动读取认证配置并校验会话。 */
104
+ export function getSessionUser(request) {
105
+ return verifySession(request, loadAuthConfig());
106
+ }
107
+ // ----------------------------- 登录后置:落库 + 写会话 -----------------------------
108
+ async function finalizeLogin(authUser, request, config, next) {
109
+ // 落库仅用于同步企业档案 / OAuth 令牌(users 表);用户主体键一律用 provider 侧
110
+ // 标识(sub = provider:unionId),不再把内部自增 id 写进会话。
111
+ if (isDbEnabled()) {
112
+ try {
113
+ const dbUser = await upsertUser(authUser);
114
+ if (dbUser)
115
+ await touchLastLogin(dbUser.id);
116
+ }
117
+ catch (e) {
118
+ console.error("[agent-kit auth] user upsert failed (login continues)", e instanceof Error ? e.message : e);
119
+ }
120
+ }
121
+ const sessionUser = toSessionUser(authUser);
122
+ const token = createSessionToken(sessionUser, config.sessionSecret);
123
+ const headers = new Headers();
124
+ setCookie(headers, SESSION_COOKIE, token, sessionCookieOptions(config.cookiePath, config.secureCookie));
125
+ clearCookie(headers, STATE_COOKIE, config.cookiePath, config.secureCookie);
126
+ clearCookie(headers, NEXT_COOKIE, config.cookiePath, config.secureCookie);
127
+ // 客户端免登(sso)返回 JSON;网页 OAuth 回调返回重定向。
128
+ // 注意:两个分支都必须携带同一组 headers(含会话 Set-Cookie)——直接复用 Headers 实例,
129
+ // 不能用 Object.fromEntries 展开(多个 Set-Cookie 会被合并破坏)。
130
+ const isSso = request.method === "POST";
131
+ if (isSso) {
132
+ headers.set("Content-Type", "application/json");
133
+ return new Response(JSON.stringify({ ok: true, next }), {
134
+ status: 200,
135
+ headers,
136
+ });
137
+ }
138
+ headers.set("Location", new URL(next, publicOrigin(request, config)).toString());
139
+ return new Response(null, { status: 302, headers });
140
+ }
141
+ export function createLoginHandlers(options = {}) {
142
+ const config = options.config ?? loadAuthConfig();
143
+ const mainProvider = createProvider(config);
144
+ // 按指定方式执行登录:dingtalk/feishu → 跳 OAuth 授权页(dev 免登已移除)。
145
+ // builtin(自建账号体系)留口子:登录系统尚未实现,这里直接返回 config 错误;
146
+ // 后续实现时在此分支加入「校验用户名密码 → finalizeLogin」即可。
147
+ function loginWith(request, id) {
148
+ if (id !== "dingtalk" && id !== "feishu")
149
+ return loginError(request, config, "config");
150
+ const provider = createProviderFor(config, id);
151
+ if (!provider)
152
+ return loginError(request, config, "config");
153
+ const state = generateState();
154
+ const origin = publicOrigin(request, config);
155
+ // cookiePath 去尾斜杠再拼,避免 "/" 时拼出 "//api/auth/callback"
156
+ const callbackBase = `${origin}${config.cookiePath.replace(/\/+$/, "")}/api/auth/callback`;
157
+ const redirectUri = id === "dingtalk"
158
+ ? config.dingtalk.redirectUri || callbackBase
159
+ : config.feishu.redirectUri || callbackBase;
160
+ const authUrl = provider.getAuthorizeUrl(redirectUri, state);
161
+ // 诊断锚点:打印实际回调 URI,便于核对应用后台「重定向 URL」白名单是否包含它
162
+ //(白名单不匹配是 OAuth code 换取失败的最常见原因:返回 200 + 无 access_token)
163
+ console.log(`[agent-kit auth] ${id} login redirect redirect_uri=${redirectUri} clientId=${config.dingtalk.clientId || config.feishu.appId ? "set" : "missing"}`);
164
+ const headers = new Headers();
165
+ // state cookie 携带 provider 前缀(adaptive 模式回调时据此路由到对应平台):
166
+ // 值格式 `${id}:${state}`;旧格式(无前缀,单方式遗留 cookie)回调时回退 mainProvider
167
+ setCookie(headers, STATE_COOKIE, `${id}:${state}`, stateCookieOptions(config.cookiePath, config.secureCookie));
168
+ const next = safeNext(new URL(request.url).searchParams.get("next") || "", config);
169
+ setCookie(headers, NEXT_COOKIE, next, stateCookieOptions(config.cookiePath, config.secureCookie));
170
+ headers.set("Location", authUrl);
171
+ return new Response(null, { status: 302, headers });
172
+ }
173
+ // GET /api/auth/login:
174
+ // - ?provider=xxx:按指定方式(登录页选择器按钮发出)
175
+ // - 无 provider:AUTH_PROVIDER 只有一个真实方式 → 直接跳转;
176
+ // 多个 → 返回清单(登录页渲染选择器);无 → 配置错误
177
+ function loginGET(request) {
178
+ const requested = new URL(request.url).searchParams.get("provider")?.toLowerCase() || "";
179
+ if (requested)
180
+ return loginWith(request, requested);
181
+ const options = getLoginOptions(config);
182
+ const total = options.providers.length;
183
+ if (total === 0)
184
+ return loginError(request, config, "config");
185
+ if (total === 1) {
186
+ return loginWith(request, options.providers[0].id);
187
+ }
188
+ // 多登录方式:正常由登录页选择器带 ?provider= 请求;这里兜底返回清单
189
+ return new Response(JSON.stringify({ message: "请选择登录方式", providers: options.providers }), { status: 200, headers: { "Content-Type": "application/json" } });
190
+ }
191
+ // GET /api/auth/callback:OAuth 回跳,用 code 换用户 → 落库 → 写会话 → 重定向
192
+ async function callbackGET(request) {
193
+ const url = new URL(request.url);
194
+ const code = url.searchParams.get("authCode") || url.searchParams.get("code");
195
+ const state = url.searchParams.get("state");
196
+ const expectedState = getCookie(request, STATE_COOKIE);
197
+ if (!code || !state || !expectedState || state !== expectedState.split(":").slice(1).join(":")) {
198
+ return loginError(request, config, "state");
199
+ }
200
+ try {
201
+ // adaptive 模式:state cookie 带 `${provider}:` 前缀,据此路由到对应平台;
202
+ // 旧格式(无前缀,单方式遗留 cookie)回退 mainProvider
203
+ const prefix = expectedState.split(":")[0];
204
+ const provider = prefix === "dingtalk" || prefix === "feishu" ? createProviderFor(config, prefix) : mainProvider;
205
+ if (!provider)
206
+ return loginError(request, config, "config");
207
+ const authUser = await provider.exchangeOAuthCode(code);
208
+ const next = safeNext(getCookie(request, NEXT_COOKIE), config);
209
+ return await finalizeLogin(authUser, request, config, next);
210
+ }
211
+ catch (e) {
212
+ console.error("[agent-kit auth] login callback failed", e);
213
+ return loginError(request, config, "network");
214
+ }
215
+ }
216
+ // POST /api/auth/sso:客户端内 JSAPI 免登(钉钉/飞书),body { code, next, provider? }
217
+ async function ssoPOST(request) {
218
+ const body = (await request.json().catch(() => ({})));
219
+ const requested = body.provider?.toLowerCase();
220
+ const targetId = requested === "dingtalk" || requested === "feishu"
221
+ ? requested
222
+ : config.method === "dingtalk" || config.method === "feishu"
223
+ ? config.method
224
+ : null;
225
+ const provider = targetId ? createProviderFor(config, targetId) : mainProvider;
226
+ if (!provider || !provider.exchangeClientCode) {
227
+ return new Response(JSON.stringify({ message: "当前认证方式不支持客户端免登" }), {
228
+ status: 400,
229
+ headers: { "Content-Type": "application/json" },
230
+ });
231
+ }
232
+ if (!body.code) {
233
+ return new Response(JSON.stringify({ message: "缺少免登码" }), {
234
+ status: 400,
235
+ headers: { "Content-Type": "application/json" },
236
+ });
237
+ }
238
+ try {
239
+ const authUser = await provider.exchangeClientCode(body.code);
240
+ const next = safeNext(body.next || "", config);
241
+ return await finalizeLogin(authUser, request, config, next);
242
+ }
243
+ catch (e) {
244
+ console.error("[agent-kit auth] client SSO failed", e);
245
+ return new Response(JSON.stringify({ message: "免登失败,请重试" }), {
246
+ status: 401,
247
+ headers: { "Content-Type": "application/json" },
248
+ });
249
+ }
250
+ }
251
+ // GET /api/auth/session:返回当前会话状态
252
+ async function sessionGET(request) {
253
+ const user = verifySession(request, config);
254
+ if (!user) {
255
+ // 诊断锚点:未登录时区分「请求没带 cookie」与「cookie 无效(签名/过期/secret 变化)」
256
+ const raw = getCookie(request, SESSION_COOKIE);
257
+ console.log(`[agent-kit auth] session unauthenticated: cookie=${raw ? "present" : "absent"}` +
258
+ (raw ? ` len=${raw.length}` : "") +
259
+ ` origin=${request.headers.get("origin") ?? "same"} cookiePath=${config.cookiePath}`);
260
+ return new Response(JSON.stringify({ authenticated: false }), {
261
+ status: 401,
262
+ headers: { "Content-Type": "application/json" },
263
+ });
264
+ }
265
+ return new Response(JSON.stringify({
266
+ authenticated: true,
267
+ user: {
268
+ name: user.name,
269
+ avatar: user.avatar,
270
+ provider: user.provider,
271
+ sub: user.sub,
272
+ unionId: user.unionId,
273
+ },
274
+ }), { status: 200, headers: { "Content-Type": "application/json" } });
275
+ }
276
+ // POST /api/auth/logout:清除会话 Cookie
277
+ async function logoutPOST() {
278
+ const headers = new Headers();
279
+ clearCookie(headers, SESSION_COOKIE, config.cookiePath);
280
+ // Headers → 普通对象(forEach 兼容 undici-types 与 lib.dom 的 Headers 类型差异)
281
+ const headerObj = {};
282
+ headers.forEach((value, key) => {
283
+ headerObj[key] = value;
284
+ });
285
+ return new Response(JSON.stringify({ ok: true }), {
286
+ status: 200,
287
+ headers: { "Content-Type": "application/json", ...headerObj },
288
+ });
289
+ }
290
+ return {
291
+ login: { GET: loginGET },
292
+ callback: { GET: callbackGET },
293
+ sso: { POST: ssoPOST },
294
+ session: { GET: sessionGET },
295
+ logout: { POST: logoutPOST },
296
+ };
297
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * agent-kit · 认证模块统一出口
3
+ *
4
+ * 公共、可复用代码全部收敛于此;具体路由接线(app 侧)只消费本出口,不感知内部实现。
5
+ */
6
+ export type { AuthUser, AuthProvider, AuthProviderId, SessionUser, AuthConfig } from "./types.js";
7
+ export { loadAuthConfig, isAuthEnabled, isMethodConfigured, parseAuthProviders } from "./config.js";
8
+ export { getLoginOptions } from "./login-options.js";
9
+ export type { LoginOption, LoginOptions } from "./login-options.js";
10
+ export { createSessionToken, verifySessionToken, generateState, toSessionUser, sessionCookieOptions, stateCookieOptions, } from "./session.js";
11
+ export { createProvider, createProviderFor, createDingTalkProvider, createFeishuProvider } from "./providers/index.js";
12
+ export { createLoginHandlers, verifySession, getSessionUser } from "./handlers.js";
13
+ export type { LoginHandlers, LoginHandlersOptions } from "./handlers.js";
14
+ export { migrate, upsertUser, findUserByProvider, touchLastLogin, getPool, isDbEnabled, createAgentSession, listAgentSessions, getAgentSession, renameAgentSession, deleteAgentSession, addAgentMessage, listAgentMessages, } from "./db.js";
15
+ export type { DbUser, AgentSessionRow, AgentMessageRow } from "./db.js";
@@ -0,0 +1,14 @@
1
+ // 配置
2
+ export { loadAuthConfig, isAuthEnabled, isMethodConfigured, parseAuthProviders } from "./config.js";
3
+ // 登录方式清单(服务端登录页决策)
4
+ export { getLoginOptions } from "./login-options.js";
5
+ // 会话与 Cookie
6
+ export { createSessionToken, verifySessionToken, generateState, toSessionUser, sessionCookieOptions, stateCookieOptions, } from "./session.js";
7
+ // 提供方工厂
8
+ export { createProvider, createProviderFor, createDingTalkProvider, createFeishuProvider } from "./providers/index.js";
9
+ // 路由工厂
10
+ export { createLoginHandlers, verifySession, getSessionUser } from "./handlers.js";
11
+ // 数据库(PostgreSQL 用户持久化)
12
+ export { migrate, upsertUser, findUserByProvider, touchLastLogin, getPool, isDbEnabled,
13
+ // 会话与消息(agent_sessions / agent_messages)
14
+ createAgentSession, listAgentSessions, getAgentSession, renameAgentSession, deleteAgentSession, addAgentMessage, listAgentMessages, } from "./db.js";
@@ -0,0 +1,14 @@
1
+ import type { AuthConfig } from "./types.js";
2
+ export interface LoginOption {
3
+ /** 登录方式标识(OAuth 平台 / 自建 builtin;builtin 待实现,类型上先留口子) */
4
+ id: "dingtalk" | "feishu" | "builtin";
5
+ /** 展示名(登录页按钮文案) */
6
+ name: string;
7
+ }
8
+ export interface LoginOptions {
9
+ /** 可用的真实登录方式(已过滤未配置项),空表示无可点选的登录按钮 */
10
+ providers: LoginOption[];
11
+ /** 会话 Cookie 路径(= Next basePath,如 /agent),登录页跳转均基于它 */
12
+ cookiePath: string;
13
+ }
14
+ export declare function getLoginOptions(config?: AuthConfig): LoginOptions;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * agent-kit · 登录方式清单(服务端)
3
+ *
4
+ * 由 AUTH_PROVIDER 解析出「登录页应如何呈现」的完整决策输入:
5
+ * - providers:可用的真实登录方式(OAuth 平台已按 env 过滤是否配置完整)
6
+ *
7
+ * 宿主登录页使用范式:
8
+ * const options = getLoginOptions();
9
+ * const total = options.providers.length;
10
+ * if (total === 1) {
11
+ * // 单个真实登录方式 → 服务端直接 302 到 /api/auth/login?provider=xxx
12
+ * }
13
+ * // total > 1 → 渲染 <LoginPage providers={options.providers} />
14
+ */
15
+ import { loadAuthConfig, isMethodConfigured } from "./config.js";
16
+ const OPTION_NAMES = {
17
+ dingtalk: "钉钉登录",
18
+ feishu: "飞书登录",
19
+ // builtin:自建账号登录(待实现;isMethodConfigured 恒 false 期间不会出现在清单里)
20
+ builtin: "账号登录",
21
+ };
22
+ export function getLoginOptions(config) {
23
+ const cfg = config ?? loadAuthConfig();
24
+ const providers = [];
25
+ const seen = new Set();
26
+ for (const m of cfg.methods) {
27
+ if (m === "none")
28
+ continue;
29
+ // adaptive:展开为两个候选 OAuth 平台,各自按 env 是否齐全过滤。
30
+ // 展开后的清单驱动登录页:钉钉/飞书客户端内自动定向,普通浏览器渲染选择器。
31
+ if (m === "adaptive") {
32
+ for (const cand of ["dingtalk", "feishu"]) {
33
+ if (!isMethodConfigured(cfg, cand))
34
+ continue;
35
+ if (seen.has(cand))
36
+ continue;
37
+ seen.add(cand);
38
+ providers.push({ id: cand, name: OPTION_NAMES[cand] });
39
+ }
40
+ continue;
41
+ }
42
+ if (!isMethodConfigured(cfg, m))
43
+ continue; // OAuth env 不全 / dev 未开启 → 不可选
44
+ if (seen.has(m))
45
+ continue;
46
+ seen.add(m);
47
+ providers.push({ id: m, name: OPTION_NAMES[m] });
48
+ }
49
+ return { providers, cookiePath: cfg.cookiePath };
50
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * agent-kit · 钉钉认证提供方
3
+ *
4
+ * 参考 D:\Project\hotline 的钉钉登录实现,提炼为与框架无关的纯逻辑:
5
+ * - 网页 OAuth:login.dingtalk.com 授权 → 用 authCode 换 userAccessToken → 读 contact/users/me
6
+ * - 客户端内 JSAPI 免登:dd.requestAuthCode 拿到 code → 用企业凭证换 app token →
7
+ * getuserinfo 拿 userid → topapi/v2/user/get 拿档案
8
+ * 全部归一化为 AuthUser,具体路由接线在 app 侧。
9
+ */
10
+ import type { AuthProvider } from "../types.js";
11
+ export interface DingTalkProviderConfig {
12
+ clientId: string;
13
+ clientSecret: string;
14
+ corpId: string;
15
+ }
16
+ export declare function createDingTalkProvider(cfg: DingTalkProviderConfig): AuthProvider;
@@ -0,0 +1,177 @@
1
+ const OAUTH_AUTHORIZE = "https://login.dingtalk.com/oauth2/auth";
2
+ const API = "https://api.dingtalk.com";
3
+ const OAPI = "https://oapi.dingtalk.com";
4
+ export function createDingTalkProvider(cfg) {
5
+ const id = "dingtalk";
6
+ function isConfigured() {
7
+ return Boolean(cfg.clientId && cfg.clientSecret && cfg.corpId);
8
+ }
9
+ function getAuthorizeUrl(redirectUri, state) {
10
+ const url = new URL(OAUTH_AUTHORIZE);
11
+ url.searchParams.set("redirect_uri", redirectUri);
12
+ url.searchParams.set("response_type", "code");
13
+ url.searchParams.set("client_id", cfg.clientId);
14
+ url.searchParams.set("scope", "openid");
15
+ url.searchParams.set("prompt", "consent");
16
+ url.searchParams.set("state", state);
17
+ return url.toString();
18
+ }
19
+ /** 网页 OAuth:authCode → userAccessToken → 通讯录个人信息(contact/users/me)。 */
20
+ async function exchangeOAuthCode(code) {
21
+ const tokenRes = await fetch(`${API}/v1.0/oauth2/userAccessToken`, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({
25
+ clientId: cfg.clientId,
26
+ clientSecret: cfg.clientSecret,
27
+ code,
28
+ grantType: "authorization_code",
29
+ }),
30
+ cache: "no-store",
31
+ });
32
+ const tokenJson = (await tokenRes.json().catch(() => ({})));
33
+ if (!tokenRes.ok || !tokenJson.accessToken) {
34
+ throw new Error(`钉钉 userAccessToken 换取失败 (${tokenRes.status})`);
35
+ }
36
+ // 校验登录归属组织(防止非本企业账号登录)
37
+ if (cfg.corpId && tokenJson.corpId && tokenJson.corpId !== cfg.corpId) {
38
+ throw new Error("当前钉钉账号不属于本企业组织");
39
+ }
40
+ const profileRes = await fetch(`${API}/v1.0/contact/users/me`, {
41
+ headers: { "x-acs-dingtalk-access-token": tokenJson.accessToken },
42
+ cache: "no-store",
43
+ });
44
+ const p = (await profileRes.json().catch(() => ({})));
45
+ const unionId = p.unionId || "";
46
+ const openId = p.openId || "";
47
+ // OAuth 路径的 /v1.0/contact/users/me 只返回 unionId/openId,不返回企业内 userid;
48
+ // 发送到钉钉会话(message/send_to_conversation 的 sender)必须用 userid,这里用
49
+ // unionId 经 getbyunionid 实时解析并写入 userCode,避免用户表 user_code 为空导致分享失败。
50
+ let userCode = "";
51
+ if (unionId) {
52
+ try {
53
+ const appToken = await getAppToken();
54
+ userCode = await resolveUserIdByUnionId(appToken, unionId);
55
+ }
56
+ catch {
57
+ // 解析失败不阻断登录;发送链路还有 unionId 兜底
58
+ }
59
+ }
60
+ if (!profileRes.ok || (!unionId && !openId)) {
61
+ // OAuth code + accessToken 已证明钉钉身份,缺通讯录权限不应让合法登录失败
62
+ console.warn("[dingtalk] profile 不可读,仅以已验证身份继续登录");
63
+ return {
64
+ provider: id,
65
+ unionId: unionId || `verified:${tokenJson.accessToken.slice(0, 16)}`,
66
+ openId,
67
+ userCode,
68
+ name: "钉钉用户",
69
+ raw: { ...p, _verified: true },
70
+ };
71
+ }
72
+ return {
73
+ provider: id,
74
+ unionId,
75
+ openId,
76
+ userCode,
77
+ name: p.nick || p.name || "钉钉用户",
78
+ avatar: p.avatarUrl || "",
79
+ mobile: p.mobile || "",
80
+ email: p.email || "",
81
+ raw: p,
82
+ };
83
+ }
84
+ /** 企业应用 app token(client_credentials)。 */
85
+ async function getAppToken() {
86
+ const appTokenRes = await fetch(`${API}/v1.0/oauth2/${encodeURIComponent(cfg.corpId)}/token`, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({
90
+ client_id: cfg.clientId,
91
+ client_secret: cfg.clientSecret,
92
+ grant_type: "client_credentials",
93
+ }),
94
+ cache: "no-store",
95
+ });
96
+ const appTokenJson = (await appTokenRes.json().catch(() => ({})));
97
+ if (!appTokenRes.ok || !appTokenJson.access_token) {
98
+ throw new Error(`钉钉 app token 换取失败 (${appTokenRes.status})`);
99
+ }
100
+ return appTokenJson.access_token;
101
+ }
102
+ /** 根据 unionId 解析企业内 userid(POST /topapi/v2/user/getbyunionid;旧路径 /topapi/user/getbyunionid 自动回退)。 */
103
+ async function resolveUserIdByUnionId(appToken, unionId) {
104
+ const body = JSON.stringify({ unionid: unionId });
105
+ for (const path of ["topapi/v2/user/getbyunionid", "topapi/user/getbyunionid"]) {
106
+ try {
107
+ const res = await fetch(`${OAPI}/${path}?access_token=${encodeURIComponent(appToken)}`, {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body,
111
+ cache: "no-store",
112
+ });
113
+ const json = (await res.json().catch(() => ({})));
114
+ if (res.ok && json.errcode === 0 && json.result?.userid)
115
+ return json.result.userid;
116
+ }
117
+ catch {
118
+ // 换路径重试
119
+ }
120
+ }
121
+ return "";
122
+ }
123
+ /** 客户端内 JSAPI 免登:code → app token → getuserinfo(userid) → user/get(档案)。 */
124
+ async function exchangeClientCode(code) {
125
+ const accessToken = await getAppToken();
126
+ const infoRes = await fetch(`${OAPI}/topapi/v2/user/getuserinfo?access_token=${encodeURIComponent(accessToken)}`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ code }),
130
+ cache: "no-store",
131
+ });
132
+ const infoJson = (await infoRes.json().catch(() => ({})));
133
+ const userId = infoJson.result?.userid;
134
+ if (!infoRes.ok || infoJson.errcode !== 0 || !userId) {
135
+ throw new Error(`钉钉免登码验证失败 (${infoJson.errcode})`);
136
+ }
137
+ // 拉通讯录档案(缺权限时降级,不阻断登录)
138
+ let profile = {};
139
+ try {
140
+ const profileRes = await fetch(`${OAPI}/topapi/v2/user/get?access_token=${encodeURIComponent(accessToken)}`, {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ userid: userId, language: "zh_CN" }),
144
+ cache: "no-store",
145
+ });
146
+ const profileJson = (await profileRes.json().catch(() => ({})));
147
+ if (profileRes.ok && profileJson.errcode === 0 && profileJson.result) {
148
+ profile = profileJson.result;
149
+ }
150
+ else {
151
+ console.warn("[dingtalk] 免登档案读取不可用,仅以 userid 建会话");
152
+ }
153
+ }
154
+ catch (e) {
155
+ console.warn("[dingtalk] 免登档案读取异常", e instanceof Error ? e.message : e);
156
+ }
157
+ return {
158
+ provider: id,
159
+ unionId: String(profile.unionid || userId),
160
+ openId: "",
161
+ userCode: userId,
162
+ name: String(profile.name || "钉钉用户"),
163
+ avatar: String(profile.avatar || ""),
164
+ mobile: String(profile.mobile || ""),
165
+ deptName: Array.isArray(profile.dept_id_list) ? undefined : undefined,
166
+ raw: profile,
167
+ };
168
+ }
169
+ return {
170
+ id,
171
+ supportsClientSso: true,
172
+ isConfigured,
173
+ getAuthorizeUrl,
174
+ exchangeOAuthCode,
175
+ exchangeClientCode,
176
+ };
177
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * agent-kit · 飞书认证提供方
3
+ *
4
+ * 与钉钉对称实现,统一归一到 AuthUser:
5
+ * - 网页 OAuth:open.feishu.cn 授权 → app_access_token → OIDC user_access_token → user_info
6
+ * - 客户端内 JSSDK 免登:tt.requestAuthCode 拿到的 code 同样走 OIDC 换取(飞书 code 通用)
7
+ * 默认域名 open.feishu.cn(中国区),可用 FS_BASE_URL 覆盖(国际版 app.feishu.com)。
8
+ */
9
+ import type { AuthProvider } from "../types.js";
10
+ export interface FeishuProviderConfig {
11
+ appId: string;
12
+ appSecret: string;
13
+ baseUrl: string;
14
+ }
15
+ export declare function createFeishuProvider(cfg: FeishuProviderConfig): AuthProvider;