@vibe-lark/larkpal 0.1.11 → 0.1.13

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 (2) hide show
  1. package/dist/main.mjs +149 -5
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -1029,18 +1029,28 @@ var SessionProcessManager = class {
1029
1029
  processEnv.LARKPAL_USER_NAME = userContext.userName;
1030
1030
  processEnv.LARKPAL_CREDENTIAL_DIR = userContext.credentialDir;
1031
1031
  processEnv.LARKPAL_IS_TENANT = userContext.isTenantIdentity ? "1" : "0";
1032
+ const disableUserAuth = process.env.LARKPAL_DISABLE_USER_AUTH === "1" || process.env.LARKPAL_DISABLE_USER_AUTH === "true";
1032
1033
  if (!userContext.isTenantIdentity) try {
1033
1034
  const vault = new CredentialVault(userContext.credentialDir);
1034
1035
  await vault.load();
1035
1036
  const credentialEnvVars = await vault.getAllCredentialsAsEnv();
1036
1037
  Object.assign(processEnv, credentialEnvVars);
1037
- const larkCred = await vault.getCredential("lark");
1038
- if (larkCred?.user_access_token && typeof larkCred.user_access_token === "string") processEnv.LARK_USER_ACCESS_TOKEN = larkCred.user_access_token;
1038
+ if (!disableUserAuth) {
1039
+ const larkCred = await vault.getCredential("lark");
1040
+ if (larkCred?.user_access_token && typeof larkCred.user_access_token === "string") processEnv.LARK_USER_ACCESS_TOKEN = larkCred.user_access_token;
1041
+ } else {
1042
+ delete processEnv.LARK_USER_ACCESS_TOKEN;
1043
+ log$25.info("LARKPAL_DISABLE_USER_AUTH 已启用,跳过飞书 user_access_token 注入", {
1044
+ sessionId,
1045
+ userId: userContext.userId
1046
+ });
1047
+ }
1039
1048
  log$25.info("用户凭证已注入到 CC 进程环境", {
1040
1049
  sessionId,
1041
1050
  userId: userContext.userId,
1042
1051
  credentialCount: Object.keys(credentialEnvVars).length,
1043
- hasLarkUserToken: !!processEnv.LARK_USER_ACCESS_TOKEN
1052
+ hasLarkUserToken: !!processEnv.LARK_USER_ACCESS_TOKEN,
1053
+ disableUserAuth
1044
1054
  });
1045
1055
  } catch (err) {
1046
1056
  log$25.warn("用户凭证加载失败,CC 进程将使用默认环境", {
@@ -2870,7 +2880,8 @@ async function fetchFromApplicationApi(token) {
2870
2880
  return {
2871
2881
  appName: zhInfo?.name || app.app_name || "",
2872
2882
  avatarUrl: app.avatar_url,
2873
- description: zhInfo?.description || app.description
2883
+ description: zhInfo?.description || app.description,
2884
+ helpDocUrl: zhInfo?.help_use
2874
2885
  };
2875
2886
  } catch (err) {
2876
2887
  log$22.warn("application/v6 API 请求异常", { error: err instanceof Error ? err.message : String(err) });
@@ -2926,9 +2937,100 @@ async function getTenantAccessToken(appId, appSecret) {
2926
2937
  return null;
2927
2938
  }
2928
2939
  }
2940
+ /**
2941
+ * 从飞书云文档 URL 中解析文档 token
2942
+ *
2943
+ * 支持的 URL 格式:
2944
+ * - https://xxx.feishu.cn/docx/{token}
2945
+ * - https://xxx.feishu.cn/wiki/{token}
2946
+ * - https://xxx.larksuite.com/docx/{token}
2947
+ */
2948
+ function parseDocTokenFromUrl(url) {
2949
+ try {
2950
+ const parts = new URL(url).pathname.split("/").filter(Boolean);
2951
+ for (let i = 0; i < parts.length; i++) {
2952
+ if (parts[i] === "docx" && parts[i + 1]) return {
2953
+ token: parts[i + 1],
2954
+ type: "docx"
2955
+ };
2956
+ if (parts[i] === "wiki" && parts[i + 1]) return {
2957
+ token: parts[i + 1],
2958
+ type: "wiki"
2959
+ };
2960
+ }
2961
+ return null;
2962
+ } catch {
2963
+ return null;
2964
+ }
2965
+ }
2966
+ /**
2967
+ * 从飞书云文档读取纯文本内容(作为人设文档)
2968
+ *
2969
+ * 对于 wiki 类型,先通过 wiki API 获取实际的 doc token,再读取内容。
2970
+ */
2971
+ async function fetchDocContent(docUrl, accessToken) {
2972
+ const parsed = parseDocTokenFromUrl(docUrl);
2973
+ if (!parsed) {
2974
+ log$22.warn("无法从 URL 中解析文档 token", { url: docUrl });
2975
+ return null;
2976
+ }
2977
+ log$22.info("开始读取人设文档", {
2978
+ url: docUrl,
2979
+ type: parsed.type,
2980
+ token: parsed.token
2981
+ });
2982
+ let docToken = parsed.token;
2983
+ if (parsed.type === "wiki") {
2984
+ const realToken = await resolveWikiNodeToDocToken(parsed.token, accessToken);
2985
+ if (!realToken) log$22.warn("wiki 节点解析失败,尝试直接使用 token 读取");
2986
+ else docToken = realToken;
2987
+ }
2988
+ try {
2989
+ const data = await (await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
2990
+ log$22.info("文档 raw_content API 响应", {
2991
+ code: data.code,
2992
+ msg: data.msg,
2993
+ contentLength: data.data?.content?.length
2994
+ });
2995
+ if (data.code !== 0 || !data.data?.content) {
2996
+ log$22.warn("读取文档内容失败", {
2997
+ code: data.code,
2998
+ msg: data.msg,
2999
+ docToken
3000
+ });
3001
+ return null;
3002
+ }
3003
+ return data.data.content.trim();
3004
+ } catch (err) {
3005
+ log$22.error("读取文档内容异常", {
3006
+ error: err instanceof Error ? err.message : String(err),
3007
+ docToken
3008
+ });
3009
+ return null;
3010
+ }
3011
+ }
3012
+ /** 将 wiki 节点 token 解析为实际的文档 token */
3013
+ async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
3014
+ try {
3015
+ const data = await (await fetch(`https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${wikiToken}`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
3016
+ log$22.info("wiki get_node API 响应", {
3017
+ code: data.code,
3018
+ msg: data.msg,
3019
+ objType: data.data?.node?.obj_type
3020
+ });
3021
+ if (data.code !== 0 || !data.data?.node?.obj_token) return null;
3022
+ return data.data.node.obj_token;
3023
+ } catch (err) {
3024
+ log$22.warn("wiki get_node 请求异常", { error: err instanceof Error ? err.message : String(err) });
3025
+ return null;
3026
+ }
3027
+ }
2929
3028
  /** CLAUDE.md 中的应用信息区块标记 */
2930
3029
  const APP_INFO_START = "<!-- APP_INFO_START -->";
2931
3030
  const APP_INFO_END = "<!-- APP_INFO_END -->";
3031
+ /** CLAUDE.md 中的人设文档区块标记 */
3032
+ const PERSONA_DOC_START = "<!-- PERSONA_DOC_START -->";
3033
+ const PERSONA_DOC_END = "<!-- PERSONA_DOC_END -->";
2932
3034
  /**
2933
3035
  * 将应用信息同步到 ~/.claude/CLAUDE.md
2934
3036
  *
@@ -2959,6 +3061,38 @@ async function syncAppInfoToClaudeMd(appInfo) {
2959
3061
  hasAvatar: !!appInfo.avatarUrl
2960
3062
  });
2961
3063
  }
3064
+ /**
3065
+ * 将人设文档内容同步到 ~/.claude/CLAUDE.md
3066
+ *
3067
+ * 在文件中维护一个 PERSONA_DOC 标记区块,内容来源于飞书云文档。
3068
+ * 如果文件中已有标记区块则替换,否则追加到文件末尾。
3069
+ */
3070
+ async function syncPersonaDocToClaudeMd(personaContent) {
3071
+ const claudeMdPath = join$1(homedir$1(), ".claude", "CLAUDE.md");
3072
+ if (!existsSync$1(claudeMdPath)) {
3073
+ log$22.warn("CLAUDE.md 不存在,无法同步人设文档(需先同步应用信息)");
3074
+ return;
3075
+ }
3076
+ let content = await readFile$1(claudeMdPath, "utf-8");
3077
+ const personaBlock = [
3078
+ PERSONA_DOC_START,
3079
+ "## 人设与行为规范",
3080
+ "",
3081
+ "> 以下内容来自飞书人设文档,是你的核心身份定义和行为准则。",
3082
+ "",
3083
+ personaContent,
3084
+ PERSONA_DOC_END
3085
+ ].join("\n");
3086
+ const startIdx = content.indexOf(PERSONA_DOC_START);
3087
+ const endIdx = content.indexOf(PERSONA_DOC_END);
3088
+ if (startIdx !== -1 && endIdx !== -1) {
3089
+ const before = content.substring(0, startIdx);
3090
+ const after = content.substring(endIdx + 24);
3091
+ content = before + personaBlock + after;
3092
+ } else content = content.trimEnd() + "\n\n" + personaBlock + "\n";
3093
+ await writeFile$1(claudeMdPath, content, "utf-8");
3094
+ log$22.info("CLAUDE.md 人设文档已同步", { contentLength: personaContent.length });
3095
+ }
2962
3096
  /** 构建应用信息标记区块 */
2963
3097
  function buildAppInfoBlock(appInfo) {
2964
3098
  const lines = [
@@ -3002,10 +3136,20 @@ async function syncAppInfo(credentials) {
3002
3136
  return null;
3003
3137
  }
3004
3138
  await syncAppInfoToClaudeMd(appInfo);
3139
+ if (appInfo.helpDocUrl) {
3140
+ log$22.info("检测到帮助文档 URL,尝试同步人设文档", { helpDocUrl: appInfo.helpDocUrl });
3141
+ const token = await getTenantAccessToken(credentials.appId, credentials.appSecret);
3142
+ if (token) {
3143
+ const personaContent = await fetchDocContent(appInfo.helpDocUrl, token);
3144
+ if (personaContent) await syncPersonaDocToClaudeMd(personaContent);
3145
+ else log$22.warn("人设文档内容为空或读取失败,跳过同步");
3146
+ }
3147
+ }
3005
3148
  await installSyncSkill();
3006
3149
  log$22.info("应用信息同步完成", {
3007
3150
  appName: appInfo.appName,
3008
- description: appInfo.description?.substring(0, 50)
3151
+ description: appInfo.description?.substring(0, 50),
3152
+ hasPersonaDoc: !!appInfo.helpDocUrl
3009
3153
  });
3010
3154
  return appInfo;
3011
3155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lark/larkpal",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "LarkPal - Lark/Feishu bot service",
5
5
  "type": "module",
6
6
  "main": "./dist/main.mjs",