Skill: 13929110463/personal-worklist-via-feishu (version 1.0.3)

--- name: personal-worklist-via-feishu description: | 飞书多维表格个人工作台账。双层读取：Frontmatter(每次触发必读) + Body(按需读)。 【触发词】 "创建个人工作台账"/"create worklist" | "记一下工作"+内容/"add task"+内容 | "完成了"/"cancelled"/"推迟"/"defer" | "复盘"/"review" | "诊断"/"diagnose" 【每次触发必验5条】 1.语言未设置→先问用户(中文/英文/c/e)，禁止跳过 2.三项必填逐一确认(任务名称/优先级①~④/截止日期)，未提供则追问，禁止自行编造 3.来源分类自动归类(未提及→临时任务[U]，协作→协作任务[C]，周期→例行任务[R]，计划内→计划任务[P])，无需追问 4.可选字段(工作要求/链接/干系人/备注/预计时长)：用户提供则写，未提供则留空，禁止推断 5.openclaw cron add/delete已授权(2026-04-27)，无需确认直接执行 【关键约束】 init_table.js必须带--open-id(用户open_id)，否则用户无法访问表格 add_task.js首次录入成功后脚本内部自动触发cron提醒(memory判断) 禁止语言未确认前执行任何脚本 【字段速查】 优先级：①紧急且重要/②重要不紧急/③紧急不重要/④不紧急不重要 状态：待办/进行中/已完成/取消 来源：计划任务[P]/临时任务[U]/例行任务[R]/协作任务[C] --- ## ⛔ 强制执行协议（按需读取） **双层读取机制：** | 层级 | 内容 | 读取时机 | |------|------|---------| | Frontmatter(每次) | 5条必验规则+字段速查+触发词 | 每次 skill 触发时 | | Body 本节 | 分场景操作约束+禁止行为 | 遇到具体场景时 | **【分场景操作约束】按需读取 body 对应章节：** - 创建台账：node scripts/init_table.js --open-id USER_OPEN_ID --lang zh|en，--open-id 必填 - 录入任务：node scripts/add_task.js --task "名称" --priority "①" --deadline "2026-04-25" --lang zh|en，首次成功自动触发 cron - 更新状态：node scripts/update_task.js --record-id ID --status "已完成" - 进度复盘：AI 读取 list_tasks.js + PMBOK 五步复盘 - 健康诊断：node scripts/health_check.js --lang zh|en **【禁止行为】每次必验：** - 禁止语言未确认前执行任何脚本 - 禁止跳过必填字段确认（任务名称/优先级/截止日期） - 禁止为可选字段自行推断值 - 禁止自行构造飞书 API，必须用内置脚本 - 禁止执行 gateway 重启/skill 安装卸载/配置文件修改（cron 例外） - **禁止自行切换/操作锁定目标之外的工作台账** — 工作台账多维表格配置一旦确定（首次初始化并确认），配置立即锁定。此后所有任务操作（add_task / update_task / list_tasks / health_check 等）只能在该表格范围内进行；set_config / create_bitable_app 等变更操作一律禁止；如需切换目标表格，必须由 Jerry 明确授权 ## 🚨 系统操作红线 **以下操作未经用户明确确认,严禁自行执行:** | 禁止操作类型 | 示例 | 违规后果 | |-------------|------|---------| | 修改 OpenClaw 配置文件 | 修改 gateway、plugins、agents 配置 | 立即停止,等待用户指令 | | 修改系统环境变量 | PATH、OpenClaw 相关变量 | 立即停止,等待用户指令 | | 安装/卸载/更新 skill | openclaw skill install/uninstall | 立即停止,等待用户指令 | | 执行 gateway 重启 | openclaw gateway restart | 立即停止,等待用户指令 | | 修改本地文件权限 | chmod、icacls 等 | 立即停止,等待用户指令 | | 执行系统级命令 | 注册表修改、系统设置变更 | 立即停止,等待用户指令 | | **新用户首次配置完成后执行任何脚本前未确认语言** | 安装完成后跳过语言确认直接运行 health_check / init_table 等 | 立即停止语言确认流程,禁止跳过 | **✅ 例外（无需确认，可直接执行）:** - `openclaw cron add/delete` — 用于设置/删除本 skill 的定时提醒，**无需用户确认**，直接执行 **规则说明:** - "明确确认"指用户口头或书面同意,不包括沉默、超时、推测 - 若用户说"等等"、"先不要"、"让我想想",均视为**未确认**,停止操作 - 任务数据读写(add_task.js / list_tasks.js / update_task.js)不属于系统操作,无需确认 - 飞书 API 调用(读写多维表格数据)不属于系统操作,无需确认 - 定时提醒设置(cron add/delete)属于**本 skill 的正常功能扩展**，无需单独确认 **判断技巧:** > 如果一个操作会影响 OpenClaw 的运行状态、用户电脑的系统配置、或任何不可逆的系统变更,**先问用户,再执行**（cron 操作除外，见上方例外）。 --- ## 核心功能 1. **创建台账** - 通过对话引导用户创建飞书多维表格 2. **任务录入** - 强触发或弱触发将任务写入台账 3. **定时提醒** - 每天三个时间点推送工作提醒(早晨/下午/下班前) 4. **进度复盘** - 下班前自动汇总当日完成率、明日预告 ## 国际化支持 ### 语言环境检测(强制) **【强制检查点】每次创建台账或录入任务前，必须确认语言环境。** **规则:** - `preferences.json` 中无语言记录 → **必须立即询问用户**，不得跳过 - `preferences.json` 中已有语言记录 → 直接使用，**不得重复询问** - `--lang` 参数显式传入 → 直接使用，**覆盖 preferences.json** **首次使用（无语言记录）询问方式:** > "您好！我是飞书工作台账助手。在开始之前，请确认您的运行环境： > - 输入 `中文` 或 `c` → 使用中文提示词和多维表格字段 > - 输入 `英文` 或 `e` → 使用英文提示词和多维表格字段" **AI 禁止行为:** - 禁止在未确认语言前执行 `node scripts/init_table.js` - 禁止在未确认语言前执行 `node scripts/add_task.js` - 禁止在未确认语言前告知用户"创建成功"或"录入成功" - 禁止在用户已用中文/英文发送消息后，AI 自行切换到另一种语言 > 语言偏好影响 skill 的**所有输出**,包括但不限于: > - 定时提醒(早晨/下午/下班前) > - 日常任务沟通(录入确认/状态更新/复盘对话) > - 输出文本(提示语/字段名/选项值/时间格式) > - AI 说的话、问的问题、给的建议 > > **一旦确认,持续生效,不得在同一次会话中切换语言。** **确认语言后的引导流程(强制):** 用户完成语言选择后，**立即**发送以下触发词引导（不要等待用户主动询问）： 中文模式确认后发送： > 🎉 语言已确认！下面是您可以使用的工作台账功能： > > **📝 任务录入（直接说）：** > - "记一下这个工作：XXXX" → 立即录入任务 > - "创建个人工作台账" → 创建新台账 > > **🔔 定时提醒：** > - 首次录入任务后，我会引导您设置每天三个时间点的提醒（早晨 08:28 / 下午 14:00 / 下班前 17:30） > > **📋 状态更新：** > - 说"完成了"、"取消了"、"还在做"、"推迟到XX" → 自动更新表格 > > - 说"复盘" → 下班前 PMBOK 式进度复盘 > > **🔧 异常诊断：** > - 说"台账不工作了"、"诊断" → 5项健康检查 > > 要开始录入任务吗？直接告诉我任务内容即可 😊 英文模式确认后发送： > 🎉 Language confirmed! Here are your worklist commands: > > **📝 Task Entry:** > - "add task: XXXX" → Add task immediately > - "create worklist" → Create new bitable > > **🔔 Scheduled Reminders:** > - After your first task, I'll guide you to set up 3 daily reminders (08:28 / 14:00 / 17:30) > > **📋 Status Updates:** > - Say "done", "cancelled", "still working", "defer to XX" → Auto-update table > - Say "review" → PMBOK-style progress review > > **🔧 Diagnostics:** > - Say "worklist not working", "diagnose" → 5-item health check > > Ready to add a task? Just tell me what you need to do 😊 **语言切换规则:** | 场景 | 中文模式 | 英文模式 | |------|----------|----------| | 触发确认 | "记一下工作" / "创建个人工作台账" | "add task" / "create worklist" | | 录入确认 | "确认录入" | "confirm" | | 字段名称 | 中文(任务名称/优先级/截止日期等) | 英文(Task Name / Priority / Deadline 等) | | 多维表格字段 | 使用中文选项值 | 使用英文选项值 | | 提示语 | 中文自然语言 | English natural language | **多维表格字段对照表:** | 中文字段 | 英文字段 | 英文选项值 | |----------|----------|------------| | 任务名称 | Task Name | - | | 来源分类 | Source Category | Planned[P]/Urgent[U]/Routine[R]/Collab[C] | | 优先级 | Priority | ① Urgent and Important / ② Not Urgent but Important / ③ Urgent but Not Important / ④ Not Urgent and Not Important | | 状态 | Status | To Do / In Progress / Done / Cancelled | | 截止日期 | Deadline | yyyy/MM/dd | | 预计时长 | Estimated Time | 如 1h、2h | | 开始时间 | Start Time | 如 9:00 | | 工作链接 | Work Link | URL | | 工作要求 | Requirements | - | | 干系人 | Stakeholder | - | | 存在问题 | Issues | - | | 备注 | Notes | - | **存储用户偏好:** 确认语言环境后,将偏好写入 `memory/preferences.json`,后续直接调用不再重复询问。 ## 第一阶段:创建台账 ### 触发条件 用户说"创建个人工作台账"(中文)或 "create worklist"(英文)时,执行以下流程。 ### Step 0: 检查必要技能与权限（强制） **在创建台账之前，必须完成以下两项检查：** **1. 检查 feishu-api-bitable 技能是否已安装：** - 检查 ~/.openclaw/workspace-robotB/skills/ 目录下是否存在 eishu-api-bitable 目录 - 或通过 openclaw CLI 查询已安装的 skills **如果技能未安装：** - 立即告知用户需要安装相关技能 - 引导用户访问 https://clawhub.ai 搜索并安装 feishu-api-bitable - 技能安装完成前不执行后续步骤 **2. 检查飞书开放平台权限配置：** - 确认在 [飞书开放平台](https://open.feishu.cn/) 的应用权限管理中已开通以下权限： - itable:app — 多维表格权限 - itable:table — 数据表权限 - itable:record — 记录读写权限 - 若权限不足，引导用户前往开放平台的「权限管理」页面申请开通 **示例提示语：** > "创建台账前需要： > 1. 安装【飞书多维表格 API 技能】— 访问 https://clawhub.ai 搜索 eishu-api-bitable 并安装 > 2. 确认【飞书开放平台权限】— 请检查您的应用是否已开通多维表格相关权限（bitable:app / bitable:table / bitable:record） > > 安装完成并确认权限后请告诉我，我将继续为您创建台账。" ### Step 1:检查飞书 API 权限 **凭证自动获取（本 skill 自动从 OpenClaw 配置读取，无需用户手动填写）：** - APP_ID → 自动从 `openclaw.json` 的 `channels.feishu.appId` 读取 - APP_SECRET → 自动从 `openclaw.json` 的 `channels.feishu.appSecret` 读取 - APP_TOKEN → 用户需提供（多用户场景，每个用户表格不同） - Table ID → 用户需提供 > 如果用户已有飞书多维表格，可直接询问链接，从链接中提取 App Token 和 Table ID。 > 如果用户提供的是已有表格链接，AI 应主动询问："您希望套用标准工作台账模板来优化这个表格吗？这样会自动创建必要的字段和选项。" > 如果用户没有表格，需要引导其调用node scripts/init_table.js 在飞书多维表格里创建标准工作台账。 **权限检查：** ```bash node scripts/check_permissions.js ``` （无需传参数，APP_ID 和 APP_SECRET 自动获取） ### Step 2:应用工作台账模板 **无论用户是新建表格还是提供已有表格链接，都应执行以下流程：** **选项 A：新用户新建表格** - 用户没有多维表格 → 必须调用 node scripts/init_table.js 创建标准工作台账 - AI 禁止直接调飞书 API 创建表格，必须通过 init_table.js 脚本执行 - 脚本内部已内置完整字段模板，AI 必须严格遵循，不自行构造 API 调用 **选项 B：用户提供已有表格** - 用户提供了多维表格链接 → AI 主动询问模板应用 - 询问语：您已有工作台账，是否需要套用标准模板来优化表格结构？（会自动创建缺失的字段和选项） - 用户同意后 → 必须调用 node scripts/init_table.js 严格按照模板创建/补全字段 - 用户拒绝 → 仅记录当前表格信息，不做修改 - AI 禁止直接调飞书 API 修改用户已有表格结构，必须通过 init_table.js 执行 **字段模板（强制）：** | 字段名 | 类型 | 选项/说明 | |--------|------|-----------| | 任务名称 | 文本 | 必填 | | 来源分类 | 单选 | 计划任务[P] / 临时任务[U] / 例行任务[R] / 协作任务[C] | | 优先级 | 单选 | ①紧急且重要 / ②重要不紧急 / ③紧急不重要 / ④不紧急不重要 | | 状态 | 单选 | 待办 / 进行中 / 已完成 / 取消 | | 截止日期 | 日期 | yyyy/MM/dd | | 预计时长 | 文本 | 如 1h、2h | | 开始时间 | 文本 | 如 9:00 或 2026-04-23 | | 工作链接 | URL | 链接格式 | | 工作要求 | 多行文本 | 刚性要求、时间节点、执行标准 | | 干系人 | 文本 | 支持多人(用中文顿号分隔) | | 存在问题 | 多行文本 | 复盘时记录阻碍原因 | | 备注 | 多行文本 | 补充说明 | ### Step 3:配置凭证 **凭证自动获取（仅 APP_TOKEN 和 TABLE_ID 需要用户手动填写）：** ```javascript const CONFIG = { APP_ID: '自动获取', // 从 openclaw.json 自动读取 APP_SECRET: '自动获取', // 从 openclaw.json 自动读取 APP_TOKEN: 'YOUR_APP_TOKEN', // 用户手动填写 TABLE_ID: 'YOUR_TABLE_ID' // 用户手动填写 }; ``` ### Step 4:初始化表格 & 配置权限（一步完成） 运行初始化脚本（**包含权限配置**，无需分两步）： ```bash node scripts/init_table.js --open-id "USER_OPEN_ID" ``` **自动化能力（本脚本执行以下操作，无需用户手动操作）：** - 检查多维表格是否存在，不存在则自动创建 - **新建表格时，自动清空默认字段和行数据**（新建表格自带一个默认文本列和示例行，必须清除后再创建我们的字段和记录） - 检查必填字段是否存在，不存在则自动创建（包括单选字段的选项配置） - **自动将用户添加为表格编辑者**（通过 --open-id 参数） - 全程中英文双语支持（根据用户语言偏好自动切换） **参数说明：** - `--open-id`: 用户的飞书 Open ID（即对话上下文中的 sender_id），**必填**，否则用户无法访问自己的表格 - `--lang`: 可选，强制指定语言（zh/en），默认从 preferences.json 读取 **权限配置闭环（强制）：** > ⚠️ **【关键】如果不传 --open-id，用户将无法访问自己刚创建的多维表格，所有数据操作都会失败。** > > AI 自动从对话上下文中读取 sender_id，拼接为 --open-id 参数调用脚本。 **AI 主动处理原则：** - 用户无需主动去申请权限 - AI 自动将用户账号加入编辑者列表 - 如遇权限 API 报错，AI 立即提供手动申请指引并持续跟进 **Step 4C: 触发词引导（完成后即执行）** 台账创建成功后，立即向用户展示本 skill 的触发词和使用说明（不要等待用户主动询问）： > **🎉 台账创建成功！** > > 您的个人工作台账已就绪 ✅ > > **📝 任务录入（直接说）：** > - "记一下这个工作：XXXX" → 立即录入任务 > - "创建个人工作台账" → 创建新台账 > > **🔔 定时提醒：** > - 首次录入任务后，我会引导您设置每天三个时间点的提醒（早晨 08:28 / 下午 14:00 / 下班前 17:30） > > **📋 状态更新：** > - 说"完成了"、"取消了"、"还在做"、"推迟到XX" → 自动更新表格 > - 说"复盘" → 下班前 PMBOK 式进度复盘 > > **🔧 异常诊断：** > - 说"台账不工作了"、"诊断" → 5项健康检查 > > 要开始录入任务吗？直接告诉我任务内容即可 😊 **注意：** 触发词引导与权限配置为两个独立步骤，必须全部完成后才告知用户台账创建完成。 **Step 4D: 确认配置定时提醒（完成后即执行，不得跳过）** 台账配置完成后，**立即**引导用户设置每日定时提醒，不得以任何理由跳过或推迟： > **🔔 定时提醒设置** > > 您的个人工作台账已就绪 ✅ 现在建议设置每日定时提醒，每天自动推送工作清单： > > - 🌅 早晨 08:28 — 当日工作重点 > - 🌤️ 下午 14:00 — 下午工作安排 > - 🌆 下班前 17:30 — PMBOK 进度复盘 > > 是否现在设置？回复「是」或「设置提醒」，我将立即为您配置三条定时任务。 **【强制约束】** - 定时提醒设置是台账初始化流程的标准步骤，**不得跳过**，不得以"稍后再说"、"我先试试录入"等理由推迟 - 只有 Jerry 明确回复「不需要」或「暂不需要」时才跳过；任何其他沉默或模糊回复均视为需要设置 - 确认设置后，执行 `node scripts/setup_reminders.js --open-id=USER_OPEN_ID --lang zh|en`（USER_OPEN_ID 从会话上下文获取，格式：`ou_xxxxxxxx`）。脚本内部已硬编码当前会话 session-key，确保推送绑定到当前飞书聊天窗口 - **推送对象必须为当前聊天窗口的 delivery target**：会话元数据中 `chat_id` 即为当前聊天窗口，直接作为 `to` 参数传入脚本；不得使用硬编码或旧配置中的用户 ID - **创建完成后必须手动验证**：执行 `cron run <jobId>` 触发一次即时推送，验证飞书是否收到；若推送失败，排查以下原因： 1. `to` 字段格式是否正确（应为 `user:ou_xxxxxxxx`） 2. `channel` 是否为 `feishu` 3. 若是 isolated session 推送，需确认 `to` 字段包含完整 open_id，而非仅 chat_id 4. 修复后再次测试，直到推送成功 - 验证通过前不得向 Jerry 报告"定时任务设置成功"；验证失败必须说明原因并给出修复方案 --- ## 第二阶段:任务录入 ### 触发规则(双轨制) **强触发 - 直接录入:** 用户说"记一下这个工作" + 任务内容 → 立即进入录入流程,询问补全必填字段。 **弱触发 - 询问确认:** 用户发送了工作相关内容但没有明确触发词 → 主动询问"是否要录入工作台账?" > 注意:用户明确表示不录入、或说"暂不需要"时,不追问。 ### 录入前确认原则(强制) **三项必填字段必须在录入前与用户逐一确认,禁止自行推断或编造:** | 字段 | 确认方式 | 如用户未提供 | |------|---------|------------| | 任务名称 | 请用户确认 | 必须追问 | | 优先级 | 请用户选择(①~④) | 必须追问 | | 截止日期 | 请用户确认 | 必须追问 | **确认话术示例:** > "好的,我来帮您录入这条工作。确认一下: > - 任务名称: [提取的内容] > - 优先级: [①紧急且重要 / ②重要不紧急 / ③紧急不重要 / ④不紧急不重要]? > - 截止日期: 哪一天?" > > 以上三项必须全部明确回复才能写入,如有任意一项用户未给出明确答案,必须追问到明确为止。 **来源分类自动归类规则:** - 用户未提及来源分类时，**不追问**，自动归类为「临时任务[U]」 - 用户提及明确的协作对象 → 归类为「协作任务[C]」 - 用户提及周期性的工作内容（如每周、每月、每季度）→ 归类为「例行任务[R]」 - 用户明确说明是计划内的工作 → 归类为「计划任务[P]」 **其他可选字段处理规则:** - 工作要求 / 工作链接 / 干系人 / 备注 / 预计时长：用户提及则记录，未提及则留空 - **录入时必须主动询问**：必填三项确认后，逐一询问"工作要求/干系人/工作链接/备注/预计时长需要录入吗？" - 禁止主动推断或补充（如用户没说干系人，AI 不得自作主张填写） - 可从用户描述中提取的内容,直接写入(无需确认) - 用户未提到的可选字段,**留空不写**,禁止推断 - 典型错误示例:用户没说干系人,AI 自作主张写"同事" → 严禁 **三项必填字段全部确认后,方可写入多维表格。** ### 相对日期处理规则(强制) **触发场景:** 用户描述中包含"今天"、"明天"、"后天"、"本周五"、"下周三"等相对日期词。 **处理步骤:** 1. 将相对日期转换为具体年月日(如"明天"→ 2026-04-24) 2. 禁止写入"今天/明天/后天"等模糊词 3. 回复时必须显式写出具体日期 + 星期(如"2026-04-25 本周五") **三步验证法(强制):** 1. 确认今天是几月几号、星期几 2. 计算加几天(目标星期 - 今天星期) 3. 反向验证:算出的日期星期几是否与目标星期匹配 ### 选项值约束（强制） **【强制】选项值约束（必须遵守）：** | 字段 | 可选值（仅以下选项，禁止自行填入其他值） | |------|---------------------------------------| | 来源分类 | 计划任务[P] / 临时任务[U] / 例行任务[R] / 协作任务[C] | | 优先级 | ①紧急且重要 / ②重要不紧急 / ③紧急不重要 / ④不紧急不重要 | | 状态 | 待办 / 进行中 / 已完成 / 取消 | **禁止行为：** - 禁止自行推断或编造选项值（不存在的选项会污染表格数据） - 若用户提供的值不在上述选项中，必须追问确认 - AI 只能使用表格中已有的选项值写入，禁止创建新选项 **字段精确性要求（强制）：** - 优先级只允许以下4个精确值（无空格）： - `①紧急且重要` - `②重要不紧急` - `③紧急不重要` - `④不紧急不重要` - 状态只允许以下4个精确值（无空格）： - `待办` - `进行中` - `已完成` - `取消` - 来源分类只允许以下4个精确值（无空格）： - `计划任务[P]` - `临时任务[U]` - `例行任务[R]` - `协作任务[C]` 如用户描述的值无法与现有选项精确匹配，先确认后再录入，不得自行猜测格式。 ### 字段放置规则 | 字段 | 放置位置 | 说明 | |------|---------|------| | 工作要求 | 「工作要求」列 | 刚性要求、时间节点、执行标准；开始时间/截止时间含具体时间点时，**自动追加时间备注**到工作要求开头 | | 备注 | 「备注」列 | 补充说明、上下文信息 | | 干系人 | 「干系人」列 | 联系人姓名(用顿号分隔) | ### 时间精度保全规则（强制） **问题背景**：飞书多维表格的「开始时间」和「截止日期」字段类型为日期，不保存具体时间点（如 14:30）。 **解决方案**：当 `--start "2026-05-20 14:30"` 或 `--deadline "2026-05-20 17:30"` 中包含时间部分时，自动提取并以 `⏰ HH:MM开始，HH:MM截止。` 格式追加到工作要求字段开头。 **示例**： - 输入：`--start "2026-05-20 14:30" --deadline "2026-05-20 17:30" --requirement "讨论拔钉插旗目标调整"` - 工作要求输出：`⏰ 14:30开始，17:30截止。讨论拔钉插旗目标调整` - 开始时间/截止日期字段：仅保存日期部分（2026-05-20） **时机**：仅在用户传入的时间字符串中含具体时间点时触发，自动执行无需询问。 --- ## 第三阶段:定时提醒 不依赖 OpenClaw cron 系统,在每次用户对话时主动检查并发送提醒。 **提醒语言规则(强制):** > 定时提醒的语言必须与用户设置的语言环境一致。 > - 中文模式 → 全程中文输出("早呀~今日重点如下...") > - 英文模式 → 全程英文输出("Good morning! Here's your today's focus...") > > 禁止在英文模式下输出中文提醒,也禁止在中文模式下输出英文提醒。 **技术实现:** - `scripts/request.js` - 统一请求层(Token 缓存 + 提前刷新、10秒超时、401 自动刷新、3次指数退避重试) - 所有 API 调用(add_task / update_task / list_tasks / init_table / health_check)均通过 request.js 发请求 - `scripts/i18n.js` 提供完整的双语文本模板(中/英文各一套) - 定时提醒内容从 `i18n.js` 的模板中取对应语言版本,确保输出语言与用户偏好一致 ### 早晨提醒(建议时间 08:28) **触发条件:** 每天 08:28 触发，必须先调用 API 判断是否发送。 **工作日判断（强制）:** 1. 调用 `http://api.jiejiariapi.com/v1/holidays/2026` 查询全年节假日数据 2. 从返回结果中查找当天日期（如 `2026-05-11`）的 `isOffDay` 字段： - `isOffDay == false`（工作日/调休上班日）→ 执行步骤1发送任务 - `isOffDay == true`（休息日/法定节日）→ 静默跳过，不发消息 - API 出错 → 发出 api 失败的消息后静默结束，不发消息 3. **绝对禁止**凭"周几"或训练数据常识跳过 API 调用！ **步骤1：发送任务** 从飞书多维表格读取开始时间<=今天且状态<>已完成且状态<>取消的任务，按以下格式输出： **输出格式(三部分):** 1. **今日重点(紧急且重要 × N):** 优先级为 1 的任务列前,注明任务名、优先级、截止时间 2. **进行中/待办任务(X 项):** 其余进行中/待办任务,表格形式列出任务名、状态、截止、优先级 3. **建议时间安排:** - **先抽 30 分钟处理:** - OA 待办审批 - 工作邮件处理 - 补充/更新当日工作清单 ### 下午提醒(建议时间 14:00) **工作日判断（强制，与早晨提醒相同逻辑）:** 1. 调用 `http://api.jiejiariapi.com/v1/holidays/2026` 查询全年节假日数据 2. 从返回结果中查找当天日期（如 `2026-05-11`）的 `isOffDay` 字段： - `isOffDay == false`（工作日/调休上班日）→ 执行步骤1发送任务 - `isOffDay == true`（休息日/法定节日）→ 静默跳过，不发消息 - API 出错 → 发出 api 失败的消息后静默结束，不发消息 3. **绝对禁止**凭"周几"或训练数据常识跳过 API 调用！ **步骤1：发送任务** 从飞书多维表格读取开始时间<=今天且状态<>已完成且状态<>取消的任务，发送下午工作计划（含上午未完成任务回顾）。 **输出格式同早晨提醒。** ### 下班前复盘(17:30) **工作日判断（强制，与早晨提醒相同逻辑）:** 1. 调用 `http://api.jiejiariapi.com/v1/holidays/2026` 查询全年节假日数据 2. 从返回结果中查找当天日期（如 `2026-05-11`）的 `isOffDay` 字段： - `isOffDay == false`（工作日/调休上班日）→ 执行复盘步骤 - `isOffDay == true`（休息日/法定节日）→ 静默跳过，不发消息 - API 出错 → 发出 api 失败的消息后静默结束，不发消息 3. **绝对禁止**凭"周几"或训练数据常识跳过 API 调用！ **执行步骤(5步,缺一不可):** **1 主动拉取任务数据** 从飞书多维表格获取截止日期 = 今天 且 状态 ≠ 已完成 且 状态 ≠ 取消的所有任务。 **2 量化复盘(截止日期 = 今天)** > 格式:"今日工作清单共 X 项(截止日期为今天),完成 X 项,完成率 X%" **3 未完成任务逐一决策** 对每项截止日期为今天且状态 ≠ 已完成的任务,逐一确认后续计划: - **推迟截止日期** → 记录新的截止日期 - **确认取消闭环** → 记录取消原因,状态改为「取消」 - **已有后续计划** → 记录后续计划和时间节点 **4 阻碍分析与工作建议** - 询问用户各未完成任务中存在的问题或阻碍 - 将问题填写到多维表格的「存在问题」字段 - 根据情况给出专业建议: - 风险应对:规避(消除)/ 转移(转嫁)/ 缓解(降低)/ 接受(承担) - 进度纠偏:赶工 / 快速跟进 / 调整范围 **5 明日预告** 预告明天重要且紧急的工作(状态 ≠ 已完成),提醒做好工作时间安排。 --- ## 第四阶段:状态更新 ### 状态更新双路径(用户自主选择) **路径一:对话驱动更新(AI 协助)** 通过与 AI 对话,告知状态变更指令,AI 自动调用 update_task.js 写入多维表格。 > 适用场景:口头汇报、复盘讨论、进度同步 **路径二:直接手动更新(用户自管理)【优选】** 用户登录飞书多维表格,直接在单元格中编辑任务状态、存在问题、截止日期等信息。 > 适用场景:用户习惯直接操作表格、时间紧迫时、快速批量修改 **说明:** - 两种路径可并存,由用户根据自身工作习惯自主选择 - 路径二为【优选】,因为手动操作最直接、无歧义、无需等待 AI 响应 - AI 在路径一种的角色是协助者而非主导者,目的是降低用户的操作成本 ### 主动闭环原则(强制) 用户给出任何状态变更指令时,**立即执行 update_task.js 更新多维表格**,不得只做口头回复。 **触发场景:** - 用户说"完成了" → 立即更新为「已完成」 - 用户说"取消了" / "闭环了" → 立即更新为「取消」 - 用户说"还在做" → 立即更新为「进行中」 - 用户说"推迟到XX" → 立即更新截止日期(相对日期要三步验证 + 回显确认) - 复盘时用户说明了阻碍原因 → 立即写入「存在问题」字段 **禁止行为:** 听到状态变更只说"好的收到"而不操作表格。 --- ## 经验教训库(来自 jerry-task-manager 实操总结) 以下规则已验证有效,新用户应直接遵守,避免重复踩坑: | 日期 | 场景 | 规则 | |------|------|------| | 2026-04-23 | 日期转换跳步 | 相对日期必须三步验证,防止思维跳格 | | 2026-04-23 | 日期回显 | 更新后必须写具体日期+星期,让用户确认 | | 2026-04-23 | 明日预告过滤 | 明日预告只包含状态≠已完成的任务,已完成的不出现 | | 2026-04-23 | 字段放置 | 工作要求列放刚性要求,备注列放补充说明,禁止放反 | | 2026-04-24 | 三项必填确认 | 任务名称/优先级/截止日期必须逐一与用户确认，禁止自行推断或编造；来源分类自动归类，不追问 | | 2026-04-24 | 禁止过度推断 | 用户未提到的可选字段留空,禁止凭想象填写(如用户没说干系人就写"同事") | | 2026-04-22 | 状态闭环 | 用户说"完成了"立即更新表格,不等二次提醒 | | 2026-04-22 | 相对日期 | 禁止写"明天"等模糊词,必须写具体日期 | | 2026-04-24 | 脚本优先 | 优先使用 Skill 内置脚本(如 init_table.js),禁止自行构造 API 调用,确保用户体验一致 | | 2026-04-21 | 优先级精确值 | 只允许四个精确值,匹配失败会污染表格选项 | | 2026-05-07 | cron 超时 | 下班前复盘(PMBOK五步)执行时间超过120s → 需将timeout设置为300s以上 | | 2026-05-07 | 相对日期转换 | 今天/明天/后天等必须转换为具体日期再写入表格，禁止模糊词入库 | | 2026-05-08 | cron任务状态筛选bug | cron提醒消息中"状态<>已完成"未生效，已完成任务也被推送。必须在时间条件基础上叠加 `status !== '已完成' && status !== '取消'` 过滤，不能依赖模糊条件 | | 2026-05-09 | 调休判断必须调用API | 判断工作日必须调用 `http://api.jiejiariapi.com/v1/holidays/2026` 查询 `isOffDay` 字段：false=工作日/调休上班日发送，true=休息日/节日静默，API失败需发失败消息后静默。绝对禁止凭"周几"或 LLM 训练数据常识跳过 API 调用！ | | 2026-05-09 | URL字段放置 | 用户描述中含URL时必须写入「工作链接」字段，禁止混入「备注」；录入前扫描任务内容，自动识别并分流 URL | | 2026-05-11 | API年份限制 | jiejiariapi 免费额度仅支持 2026 年，2027 年数据需等开放后手动切换；切换后需同步更新三条 cron 任务的 message 模板 | | 2026-05-12 | 定时任务 Prompt 精简原则 | cron job 的 payload message 应精简为核心指令（工作日判断→飞书查询→输出格式），删除冗余步骤说明和格式模板，可减少约 30% 推理耗时。精简后的 prompt 已更新到 `scripts/setup_reminders.js` 的 REMINDERS 配置中，作为未来新建定时任务的配置基准线 | | 2026-05-14 | 量化复盘查询不对称bug | 量化复盘使用「截止日期」作为统计口径，但cron旧prompt用「开始时间」查询未完成任务，导致统计基数不一致。修复：统一使用「截止日期」作为全部统计口径（已完成/未完成/明日预告均基于截止日期过滤）；修复query_today_tasks.js硬编码日期bug | | 2026-05-14 | 日期比对方言不一致导致统计错误 | cron旧prompt中「截止日期为今天且状态=已完成」与「开始时间<=今天且状态<>已完成且状态<>取消」使用不同字段，造成同一任务在完成统计和未完成统计中口径不一致。修复：统一用「截止日期」作为唯一口径，删除「开始时间」字段的引用 | --- ## 脚本说明 | 脚本 | 用途 | 关键参数 | |------|------|---------| | `setup_reminders.js` | 定时提醒设置引导（创建后需手动验证推送） | `--lang zh|en`, `--open-id=USER_OPEN_ID` | | `check_permissions.js` | 检查飞书 API 连通性 | `--app-id`, `--app-secret` | | `init_table.js` | 初始化表格(自动创建表格和字段,类型一致则跳过) | 无参数(自动检测语言偏好) | | `health_check.js` | 健康诊断(网络/凭证/表格/字段5项检查,使用 request.js) | 无参数(自动检测语言偏好) | | `set_config.js` | 动态更新飞书 API 凭证(支持部分更新) | `--app-id`, `--app-secret`, `--app-token`, `--table-id`, `--list` | | `request.js` | 统一请求层(Token缓存/10s超时/401刷新/3次重试) | 内部使用,供其他脚本调用 | | `add_task.js` | 添加任务记录(使用 request.js) | `--task`, `--source`, `--priority`, `--deadline`, ... `--lang` | | `update_task.js` | 更新任务记录 | `--record-id`, `--status`, `--deadline`, `--start`, `--duration`, `--notes`, `--problem`, `--stakeholders`, `--lang` | | `list_tasks.js` | 读取任务列表(根据语言偏好输出) | 无参数 | | `utils.js` | 飞书 API 工具函数(getAccessToken) | 无 | | `preferences.js` | 用户偏好读写(语言设置) | `getLanguage()`, `setLanguage(lang)` | | `i18n.js` | 国际化文本模板(定时提醒/输出文本) | `getTemplates(lang)`, `format(template, data)` | --- ## 完成标准(每条可打勾验证) - [ ] 用户说"创建个人工作台账"后,能引导完成完整的台账创建流程 - [ ] 三个必填字段（任务名称/优先级/截止日期）齐全时能立即写入多维表格，无需等待全部字段咨询完毕 - [ ] 相对日期能正确转换并回显具体日期+星期 - [ ] 定时提醒能正确筛选并输出格式正确的三段式提醒 - [ ] 下班前复盘能完成5步闭环(拉取→量化→决策→分析→预告) - [ ] 状态变更能立即执行表格更新,不只做口头回复 - [ ] 经验教训库中的规则被遵守,不重复已知的错误 --- ## 异常处理 - **API 连通性失败** → 提示用户检查 App ID / App Secret / 网络连接 - **表格字段不匹配** → 提示用户在飞书中检查字段配置 - **日期计算错误** → 使用三步验证法重新计算并向用户确认 - **权限不足** → 提示用户在飞书多维表格中给自己添加管理员权限

path: package.json
content: {
  "name": "personal-worklist-via-feishu",
  "version": "1.0.0",
  "description": "飞书多维表格个人工作台账管理系统",
  "main": "index.js",
  "directories": {
    "example": "examples"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "axios": "^1.15.2"
  }
}

path: scripts/add_permission.js
content: /**
 * 为多维表格添加编辑权限
 * 用法: node add_permission.js --open-id "USER_OPEN_ID" --perm "edit|view|full_access"
 * 
 * 功能：
 * 1. 获取 tenant_access_token
 * 2. 将指定用户添加为多维表格的编辑者
 * 
 * 使用 request.js 统一请求（带重试/超时/401自动刷新）
 */

const { getAccessToken } = require('./request');
const { CONFIG } = require('./config');
const { getLanguage, getTemplates } = require('./i18n');

// 权限级别
const PERM_LEVELS = {
  view: 'view',
  edit: 'edit',
  full_access: 'full_access'
};

async function addPermission(openId, perm = 'edit') {
  const token = await getAccessToken();
  const lang = getLanguage();
  const t = getTemplates(lang);

  console.log(t.permission?.adding || '🔄 添加多维表格编辑权限...');
  console.log(`   Open ID: ${openId}`);
  console.log(`   Permission: ${perm}`);

  try {
    const response = await require('axios').post(
      `https://open.feishu.cn/open-apis/drive/v1/permissions/${CONFIG.APP_TOKEN}/members?type=bitable`,
      {
        member_type: 'openid',
        member_id: openId,
        perm: PERM_LEVELS[perm] || 'edit'
      },
      {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json'
        },
        timeout: 10000
      }
    );

    if (response.data.code === 0) {
      console.log('✅ ' + (t.permission?.success || '权限添加成功'));
      return { success: true };
    } else {
      console.error('❌ ' + (t.permission?.failed || '权限添加失败') + `: ${response.data.msg}`);
      return { success: false, msg: response.data.msg };
    }
  } catch (error) {
    const errMsg = error.response?.data?.msg || error.message;
    console.error('❌ ' + (t.permission?.error || '权限添加异常') + `: ${errMsg}`);
    return { success: false, error: errMsg };
  }
}

// 主函数
async function main() {
  const args = process.argv.slice(2);
  let openId = null;
  let perm = 'edit';

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--open-id' && args[i + 1]) {
      openId = args[i + 1];
      i++;
    } else if (args[i] === '--perm' && args[i + 1]) {
      perm = args[i + 1];
      i++;
    } else if (args[i] === '--lang' && args[i + 1]) {
      // 语言参数会被 i18n 自动识别，这里忽略
      i++;
    }
  }

  // 如果没有传 open-id，使用配置中的默认用户ID
  if (!openId) {
    console.log('⚠️  未指定 --open-id，尝试从配置读取...');
    // 这里可以从配置文件读取默认用户ID，暂用占位符
    console.log('❌ 请通过 --open-id 参数指定用户 Open ID');
    console.log('   用法: node add_permission.js --open-id "USER_OPEN_ID" --perm "edit"');
    process.exit(1);
  }

  const result = await addPermission(openId, perm);
  process.exit(result.success ? 0 : 1);
}

// 只在直接运行时执行 main()，被 require 时不执行
if (require.main === module) {
  main();
}

module.exports = { addPermission };


path: scripts/add_task.js
content: /**
 * 添加任务到飞书多维表格
 * 
 * 【日期格式】截止日期支持：YYYY-MM-DD / YYYY/MM/DD
 * 【时间戳】日期字段以毫秒时间戳存储（飞书create API要求毫秒，update API要求秒，需分开处理）
 * 【开始时间】默认值为任务录入当天，不传则自动填充
 * 
 * 用法: node add_task.js --task "任务名称" --source "计划任务[P]" --priority "①紧急且重要" --deadline "2026-04-25" --duration "2h" --start "9:00" --notes "备注" --link "https://..." --requirement "工作要求" --stakeholders "张三,李四" [--lang zh|en]
 * 
 * 至少需要 --task（任务名称）才能执行
 * 
 * 英文模式下可用：
 * node add_task.js --task "Task name" --source "Planned[P]" --priority "① Urgent and Important" --deadline "2026-04-25" --lang "en"
 */

const { apiPost, apiGet } = require('./request');
const { CONFIG } = require('./config');
const { getLanguage, getTemplates, format } = require('./i18n');
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

// memory 目录下提醒标记文件路径
const REMINDER_DONE_FILE = path.join(__dirname, '..', 'memory', 'reminder_setup_done');

// 检查是否已完成定时提醒设置
function isReminderSetupDone() {
  return fs.existsSync(REMINDER_DONE_FILE);
}

// 标记定时提醒设置已完成
function markReminderSetupDone() {
  try {
    fs.writeFileSync(REMINDER_DONE_FILE, new Date().toISOString(), 'utf8');
    return true;
  } catch (e) {
    return false;
  }
}

// 自动执行定时提醒设置
async function autoSetupReminders(lang) {
  const t = getTemplates(lang);
  const scriptPath = path.join(__dirname, 'setup_reminders.js');

  console.log(lang === 'zh'
    ? '\n🔔 检测到首次任务录入，自动创建定时提醒...'
    : '\n🔔 First task detected, automatically setting up reminders...');

  try {
    const output = execSync(`node "${scriptPath}" --lang ${lang}`, {
      encoding: 'utf8',
      timeout: 60000,
      windowsHide: true
    });
    console.log(output);

    if (output.includes('成功') || output.includes('succeeded') || output.includes('✅')) {
      markReminderSetupDone();
      console.log(lang === 'zh'
        ? '\n✅ 定时提醒设置完成！'
        : '\n✅ Reminder setup complete!');
      return { success: true };
    } else {
      console.log(lang === 'zh'
        ? '\n⚠️ 定时提醒设置可能有失败，请检查上方输出'
        : '\n⚠️ Some reminders may have failed - please check output above');
      return { success: false };
    }
  } catch (error) {
    const errMsg = error.stdout || error.stderr || error.message;
    console.log(lang === 'zh'
      ? `❌ 定时提醒设置失败: ${errMsg}`
      : `❌ Reminder setup failed: ${errMsg}`);
    return { success: false, error: errMsg };
  }
}

function parseArgs() {
  const args = process.argv.slice(2);
  const config = {};
  for (let i = 0; i < args.length; i += 2) {
    const key = args[i].replace('--', '');
    const value = args[i + 1];
    config[key] = value;
  }
  return config;
}

// 相对日期转换为具体日期
// 触发词：今天/明天/后天/今天上午/今天下午/明天上午/明天下午 等
function parseRelativeDate(dateStr) {
  if (!dateStr || typeof dateStr !== 'string') return dateStr;
  
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  
  const lower = dateStr.toLowerCase();
  
  // 今天 / 今天上午 / 今天下午
  if (lower === '今天' || lower === 'today') {
    return formatDateStr(today);
  }
  if (lower === '明天' || lower === 'tomorrow') {
    const d = new Date(today);
    d.setDate(d.getDate() + 1);
    return formatDateStr(d);
  }
  if (lower === '后天' || lower === 'day after tomorrow') {
    const d = new Date(today);
    d.setDate(d.getDate() + 2);
    return formatDateStr(d);
  }
  
  // 明天上午 / 明天下午 / 今天上午 / 今天下午
  const amPmMatch = lower.match(/^(今天|明天|后天)(上午|下午|晚上)?$/);
  if (amPmMatch) {
    const dayWord = amPmMatch[1];
    const timePart = amPmMatch[2] || '';
    let targetDay;
    if (dayWord === '今天') targetDay = new Date(today);
    else if (dayWord === '明天') { targetDay = new Date(today); targetDay.setDate(targetDay.getDate() + 1); }
    else if (dayWord === '后天') { targetDay = new Date(today); targetDay.setDate(targetDay.getDate() + 2); }
    
    if (timePart === '上午') return formatDateStr(targetDay) + ' 09:00';
    if (timePart === '下午') return formatDateStr(targetDay) + ' 14:00';
    if (timePart === '晚上') return formatDateStr(targetDay) + ' 18:00';
    return formatDateStr(targetDay);
  }
  
  // 本周一/本周二 ... 本周日
  const weekMatch = lower.match(/^本周([一二三四五六日天])/);
  if (weekMatch) {
    const dayMap = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7 };
    const targetWeekday = dayMap[weekMatch[1]];
    const currentWeekday = today.getDay() || 7; // 周日为7
    let diff = targetWeekday - currentWeekday;
    if (diff <= 0) diff += 7;
    const targetDay = new Date(today);
    targetDay.setDate(targetDay.getDate() + diff);
    return formatDateStr(targetDay);
  }
  
  // 下周一 / 下周二 ...
  const nextWeekMatch = lower.match(/^下周([一二三四五六日天])/);
  if (nextWeekMatch) {
    const dayMap = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7 };
    const targetWeekday = dayMap[nextWeekMatch[1]];
    const currentWeekday = today.getDay() || 7;
    let diff = targetWeekday - currentWeekday + 7;
    const targetDay = new Date(today);
    targetDay.setDate(targetDay.getDate() + diff);
    return formatDateStr(targetDay);
  }
  
  return dateStr; // 非相对日期，原样返回
}

// 格式化日期为 YYYY-MM-DD
function formatDateStr(date) {
  const y = date.getFullYear();
  const m = String(date.getMonth() + 1).padStart(2, '0');
  const d = String(date.getDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
}

// 中文字段名 → 英文字段名映射
const FIELD_NAME_MAP = {
  '任务名称': 'Task Name',
  '来源分类': 'Source Category',
  '优先级': 'Priority',
  '状态': 'Status',
  '截止日期': 'Deadline',
  '预计时长': 'Estimated Time',
  '开始时间': 'Start Time',
  '工作链接': 'Work Link',
  '工作要求': 'Requirements',
  '干系人': 'Stakeholder',
  '存在问题': 'Issues',
  '备注': 'Notes'
};

// 获取多维表格实际字段名（根据语言）
function getFieldName(zhName, lang) {
  if (lang === 'en') {
    return FIELD_NAME_MAP[zhName] || zhName;
  }
  return zhName;
}

// 转换字段值（优先级/状态/来源）
function translateFieldValue(fieldName, value, lang) {
  // 不再翻译，直接返回原值（中文模式用中文值，英文模式用英文值）
  return value;
}

// 选项值白名单（必须从这些值中选择，禁止自行填入其他值）
const VALID_SOURCES = ['计划任务[P]', '临时任务[U]', '例行任务[R]', '协作任务[C]', 'Planned[P]', 'Urgent[U]', 'Routine[R]', 'Collab[C]'];
const VALID_PRIORITIES = ['①紧急且重要', '②重要不紧急', '③紧急不重要', '④不紧急不重要', '① Urgent and Important', '② Not Urgent but Important', '③ Urgent but Not Important', '④ Not Urgent and Not Important'];
const VALID_STATUSES = ['待办', '进行中', '已完成', '取消', 'To Do', 'In Progress', 'Done', 'Cancelled'];

// 验证选项值是否合法
function validateOption(fieldName, value, validOptions, lang) {
  if (!value) return { valid: true, value: value };
  if (validOptions.includes(value)) return { valid: true, value: value };
  return { valid: false, value: value };
}

async function addTask(config) {
  if (!config.task) {
    const lang = config.lang || getLanguage();
    const t = getTemplates(lang);
    console.error(lang === 'zh' ? '❌ 任务名称不能为空' : '❌ Task name is required');
    console.error(lang === 'zh' ? `   用法: node add_task.js --task "任务名称" --source "计划任务[P]" --priority "①紧急且重要" --deadline "2026-04-25"` : `   Usage: node add_task.js --task "Task name" --source "Planned[P]" --priority "① Urgent and Important" --deadline "2026-04-25"`);
    process.exit(1);
  }

  const lang = config.lang || getLanguage();

  // ================================================================
  // 【重要】飞书多维表格日期字段要求：毫秒时间戳（create API 期望毫秒）
  // 传入日期格式支持：YYYY-MM-DD / YYYY/MM/DD / MM-DD / MM/DD
  // 所有日期入库前统一转换为毫秒时间戳
  // ================================================================
  
  // 【重要】来源分类、优先级、状态必须使用表格中的选项值，禁止自行填入其他值
  // 验证来源分类
  const defaultSource = lang === 'zh' ? '计划任务[P]' : 'Planned[P]';
  const sourceInput = config.source || defaultSource;
  if (!VALID_SOURCES.includes(sourceInput)) {
    console.error(lang === 'zh' 
      ? `❌ 来源分类「${sourceInput}」不在允许的选项中。允许的选项：计划任务[P] / 临时任务[U] / 例行任务[R] / 协作任务[C]`
      : `❌ Source "${sourceInput}" is not a valid option. Valid options: Planned[P] / Urgent[U] / Routine[R] / Collab[C]`);
    process.exit(1);
  }

  // 验证优先级
  const defaultPriority = lang === 'zh' ? '②重要不紧急' : '② Not Urgent but Important';
  const priorityInput = config.priority || defaultPriority;
  if (!VALID_PRIORITIES.includes(priorityInput)) {
    console.error(lang === 'zh'
      ? `❌ 优先级「${priorityInput}」不在允许的选项中。允许的选项：①紧急且重要 / ②重要不紧急 / ③紧急不重要 / ④不紧急不重要`
      : `❌ Priority "${priorityInput}" is not a valid option. Valid options: ① Urgent and Important / ② Not Urgent but Important / ③ Urgent but Not Important / ④ Not Urgent and Not Important`);
    process.exit(1);
  }

  // 验证状态
  const defaultStatus = lang === 'zh' ? '待办' : 'To Do';
  const statusInput = config.status || defaultStatus;
  if (!VALID_STATUSES.includes(statusInput)) {
    console.error(lang === 'zh'
      ? `❌ 状态「${statusInput}」不在允许的选项中。允许的选项：待办 / 进行中 / 已完成 / 取消`
      : `❌ Status "${statusInput}" is not a valid option. Valid options: To Do / In Progress / Done / Cancelled`);
    process.exit(1);
  }
  
  // 【重要】相对日期转换：今天/明天/后天 等词必须转换为具体日期
  // 禁止写入"今天/明天/后天"等模糊词
  const parsedDeadline = parseRelativeDate(config.deadline);
  const parsedStart = parseRelativeDate(config.start);
  
  // 转换日期为时间戳（飞书需要毫秒）
  let deadlineTs = null;
  let deadlineTimeStr = null;
  if (parsedDeadline) {
    const date = new Date(parsedDeadline);
    date.setHours(0, 0, 0, 0);
    deadlineTs = date.getTime();
    // 提取截止时间中的时间部分（如 "14:30"）
    const deadlineHasTime = /\s\d{1,2}:\d{2}$/.test(parsedDeadline);
    if (deadlineHasTime) {
      deadlineTimeStr = parsedDeadline.match(/\s(\d{1,2}:\d{2})$/)?.[1] || null;
    }
  }

  // 开始时间默认值：任务录入当天（格式：YYYY-MM-DD）
  // 使用 config.start !== undefined 判断是否有传入值，避免空字符串覆盖默认值
  const today = new Date();
  const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
  const startTimeRaw = parsedStart !== undefined ? parsedStart : todayStr;
  // 提取开始时间中的时间部分（如 "14:30"）
  let startTimeStr = null;
  const startHasTime = /\s\d{1,2}:\d{2}$/.test(startTimeRaw);
  if (startHasTime) {
    startTimeStr = startTimeRaw.match(/\s(\d{1,2}:\d{2})$/)?.[1] || null;
  }
  // 转换为毫秒时间戳（处理 YYYY-MM-DD HH:MM 格式，保留具体时间）
  const startTimeTs = (() => {
    if (!startTimeRaw) return null;
    const d = new Date(startTimeRaw);
    // 如果 parsed date 包含时间部分（如 "2026-05-09 14:00"），保留时间
    // 否则默认 00:00:00
    if (!startHasTime) {
      d.setHours(0, 0, 0, 0);
    }
    return d.getTime();
  })();

  // 构建时间备注：当开始时间或截止时间含具体时间点时，同步写入工作要求
  const timeRemarkParts = [];
  if (startTimeStr) timeRemarkParts.push(startTimeStr + '开始');
  if (deadlineTimeStr) timeRemarkParts.push(deadlineTimeStr + '截止');
  const timeRemark = timeRemarkParts.length > 0 ? '⏰ ' + timeRemarkParts.join('，') + '。' : '';

  // 将时间备注追加到工作要求开头
  const finalRequirement = timeRemark + (config.requirement || '');

  // 获取字段名（根据语言）
  const fieldName_task = getFieldName('任务名称', lang);
  const fieldName_source = getFieldName('来源分类', lang);
  const fieldName_priority = getFieldName('优先级', lang);
  const fieldName_status = getFieldName('状态', lang);
  const fieldName_deadline = getFieldName('截止日期', lang);
  const fieldName_duration = getFieldName('预计时长', lang);
  const fieldName_start = getFieldName('开始时间', lang);
  const fieldName_notes = getFieldName('备注', lang);
  const fieldName_link = getFieldName('工作链接', lang);
  const fieldName_req = getFieldName('工作要求', lang);
  const fieldName_stakeholder = getFieldName('干系人', lang);

  // 转换字段值
  const source = translateFieldValue('来源分类', config.source || (lang === 'zh' ? '计划任务[P]' : 'Planned[P]'), lang);
  const priority = translateFieldValue('优先级', config.priority || (lang === 'zh' ? '②重要不紧急' : '② Important'), lang);
  const status = translateFieldValue('状态', config.status || (lang === 'zh' ? '待办' : 'To Do'), lang);

  const fields = {};
  fields[fieldName_task] = config.task;
  fields[fieldName_source] = source;
  fields[fieldName_priority] = priority;
  fields[fieldName_status] = status;
  fields[fieldName_duration] = config.duration || '';
  fields[fieldName_start] = startTimeTs;

  if (config.notes) {
    fields[fieldName_notes] = config.notes;
  }

  if (deadlineTs) {
    fields[fieldName_deadline] = deadlineTs;
  }

  if (config.link) {
    fields[fieldName_link] = { "link": config.link, "text": config.link };
  }

  if (finalRequirement) {
    fields[fieldName_req] = finalRequirement;
  }

  // 允许 --stakeholder 和 --stakeholders 两种参数名
  if (config.stakeholder !== undefined && config.stakeholders === undefined) {
    config.stakeholders = config.stakeholder;
  }

  if (config.stakeholders) {
    fields[fieldName_stakeholder] = config.stakeholders;
  }

  const result = await apiPost(
    `/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${CONFIG.TABLE_ID}/records`,
    { fields }
  );

  return result;
}

async function main() {
  try {
    const config = parseArgs();
    const lang = config.lang || getLanguage();

    // 【强制检查】语言环境必须已设置（通过 --lang 参数或 preferences.json）
    // 如果语言未设置，脚本必须退出并报错，禁止在语言不明的情况下录入任务
    if (!lang) {
      console.error('============================================================');
      console.error('❌ 错误：语言环境未设置');
      console.error('');
      console.error('在录入任务之前，必须先确认语言环境。');
      console.error('');
      console.error('请先告诉 AI 您的语言偏好：');
      console.error('  - 输入 "中文" 或 "c" → 使用中文');
      console.error('  - 输入 "英文" 或 "e" → Use English');
      console.error('');
      console.error('示例：告诉 AI "我想用中文" 或 "I prefer English"');
      console.error('============================================================');
      process.exit(1);
    }

    const t = getTemplates(lang);

    console.log(lang === 'zh' ? '🔄 录入任务:' : '🔄 Adding task:', config.task);

    // 【重要】首次录入检测：写入前先统计当前任务数
    const existingResult = await apiGet(`/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${CONFIG.TABLE_ID}/records`);
    const existingCount = existingResult.data?.items?.length || 0;


    const result = await addTask(config);


    if (result.code === 0) {
      console.log(t.confirm.insertSuccess);
      console.log(format(t.confirm.recordId, { id: result.data?.record?.record_id }));

      // 【强制】首条任务录入成功后，自动创建定时提醒（仅执行一次）
      if (existingCount === 0 && !isReminderSetupDone()) {
        await autoSetupReminders(lang);
      }
    } else {
      console.error(format(t.confirm.insertFailed, { msg: result.msg }));
      process.exit(1);
    }
  } catch (error) {
    const lang = getLanguage();
    const t = getTemplates(lang);
    console.error(format(t.confirm.insertFailed, { msg: error.response?.data?.msg || error.message }));
    process.exit(1);
  }
}

main();

path: scripts/check_all_records.js
content: const { apiGet } = require('./request');
const { CONFIG } = require('./config');

async function getAllRecords() {
  let records = [];
  let pageToken = '';
  do {
    const params = pageToken ? '?page_token=' + pageToken + '&page_size=100' : '?page_size=100';
    const r = await apiGet('/bitable/v1/apps/' + CONFIG.APP_TOKEN + '/tables/' + CONFIG.TABLE_ID + '/records' + params);
    const data = r.data?.items || r.data?.records || [];
    records = records.concat(data);
    pageToken = r.data?.page_token || '';
  } while (pageToken);
  return records;
}

getAllRecords().then(records => {
  console.log('总记录数:', records.length);
  console.log('\n=== 各字段填充情况 ===');
  
  const fields = ['任务名称', '来源分类', '优先级', '状态', '截止日期', '开始时间', '干系人', '工作要求', '工作链接', '备注', '预计时长'];
  
  fields.forEach(f => {
    const count = records.filter(r => r.fields && r.fields[f] !== undefined && r.fields[f] !== '' && r.fields[f] !== null).length;
    const pct = records.length > 0 ? Math.round(count / records.length * 100) : 0;
    console.log('  ' + f + ': ' + count + '/' + records.length + ' (' + pct + '%)');
  });
  
  console.log('\n=== 缺失可选字段的记录 ===');
  const optionalFields = ['干系人', '工作要求', '工作链接', '备注', '预计时长'];
  let missingCount = 0;
  records.forEach(r => {
    if (!r.fields) return;
    const missing = [];
    optionalFields.forEach(f => {
      const val = r.fields[f];
      if (val === undefined || val === '' || val === null) missing.push(f);
    });
    if (missing.length > 0) {
      missingCount++;
      console.log('[' + r.record_id + '] ' + r.fields['任务名称'] + ': 缺失 ' + missing.join(', '));
    }
  });
  console.log('\n共 ' + missingCount + ' 条记录存在可选字段缺失');
}).catch(console.error);

path: scripts/check_new_fields.js
content: const path = require('path');
const fs = require('fs');
const https = require('https');

// Find openclaw.json
let dir = __dirname;
for (let i = 0; i < 4; i++) { dir = path.dirname(dir); }
const openclawPath = path.join(dir, 'openclaw.json');
const openclawConfig = JSON.parse(fs.readFileSync(openclawPath, 'utf8'));
const feishu = openclawConfig?.channels?.feishu;

const data = JSON.stringify({ app_id: feishu.appId, app_secret: feishu.appSecret });
const options = {
  hostname: 'open.feishu.cn',
  path: '/open-apis/auth/v3/tenant_access_token/internal',
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
};
const req = https.request(options, (res) => {
  let body = '';
  res.on('data', c => body += c);
  res.on('end', () => {
    const [REDACTED_SECRET]).tenant_access_token;
    console.log('Token: OK');
    
    // Get fields
    const getOpts = {
      hostname: 'open.feishu.cn',
      path: [REDACTED_SECRET],
      method: 'GET',
      headers: { 'Authorization': 'Bearer ' + token }
    };
    const getReq = https.request(getOpts, (res2) => {
      let b2 = '';
      res2.on('data', c => b2 += c);
      res2.on('end', () => {
        const fields = JSON.parse(b2);
        if (fields.code === 0) {
          console.log('\nFields in new bitable:');
          fields.data.items.forEach(f => {
            console.log(`  ${f.field_name}: type=${f.type}, ui_type=${f.ui_type}`);
          });
        } else {
          console.log('Error getting fields:', b2);
        }
      });
    });
    getReq.end();
  });
});
req.write(data);
req.end();


path: scripts/check_permissions.js
content: /**
 * 检查飞书 API 连通性
 * 用法: node check_permissions.js
 * 
 * 需要先在 config.js 中配置 APP_ID 和 APP_SECRET
 */

const axios = require('axios');
const { CONFIG } = require('./config');

async function checkPermissions() {
  console.log('🔄 检查飞书 API 连通性...');
  
  try {
    const response = await axios.post(
      'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
      {
        app_id: CONFIG.APP_ID,
        app_secret: CONFIG.APP_SECRET
      },
      { timeout: 10000 }  // 10秒超时控制
    );
    
    if (response.data.tenant_access_token) {
      console.log('✅ API 连通性正常');
      console.log('   Token 获取成功');
      return { success: true, [REDACTED_SECRET] };
    }
  } catch (error) {
    console.error('❌ API 连通性检查失败:', error.response?.data?.msg || error.message);
    return { success: false, error: error.response?.data || error.message };
  }
}

async function main() {
  const result = await checkPermissions();
  if (!result.success) {
    process.exit(1);
  }
}

main();

path: scripts/config.js
content: /**
 * 飞书 API 凭证配置
 *
 * APP_ID / APP_SECRET: 自动从 OpenClaw 配置读取
 * APP_TOKEN / TABLE_ID: 用户安装后自行配置（运行 node scripts/set_config.js --app-token XXX --table-id XXX）
 *
 * 凭证说明:
 *   - APP_ID: 飞书应用的 App ID，从 OpenClaw 配置自动读取
 *   - APP_SECRET: 飞书应用的 App Secret，从 OpenClaw 配置自动读取
 *   - APP_TOKEN: 多维表格的唯一标识，从飞书多维表格 URL 中提取
 *   - TABLE_ID: 数据表的 ID，从飞书多维表格 URL 中提取
 *   - 示例 URL: https://xxx.feishu.cn/base/APP_TOKEN?table=TABLE_ID
 */

const fs = require('fs');
const path = require('path');

const CONFIG = {
  // 从 OpenClaw 配置自动读取，或环境变量
  APP_ID: process.env.FEISHU_APP_ID || '',
  APP_SECRET: process.env.FEISHU_APP_SECRET || '',
  // 用户安装后通过 node scripts/set_config.js 配置
  APP_TOKEN: '',
  TABLE_ID: ''
};

// 自动从 OpenClaw 配置读取 APP_ID 和 APP_SECRET
function loadOpenClawConfig() {
  try {
    // 向上查找最多 4 层目录
    let dir = __dirname;
    for (let i = 0; i < 5; i++) {
      const configPath = path.join(dir, 'openclaw.json');
      if (fs.existsSync(configPath)) {
        const data = JSON.parse(fs.readFileSync(configPath, 'utf8'));
        const feishu = data?.channels?.feishu;
        if (feishu?.appId && feishu?.appSecret) {
          CONFIG.APP_ID = feishu.appId;
          CONFIG.APP_SECRET = feishu.appSecret;
          return;
        }
      }
      dir = path.dirname(dir);
    }
  } catch (e) {
    // 忽略，继续使用环境变量
  }
}

// 如果环境变量中有凭证，优先使用
if (process.env.FEISHU_APP_ID) CONFIG.APP_ID = process.env.FEISHU_APP_ID;
if (process.env.FEISHU_APP_SECRET) CONFIG.APP_SECRET = process.env.FEISHU_APP_SECRET;

// 如果环境变量没有，尝试从 OpenClaw 配置读取
if (!CONFIG.APP_ID || !CONFIG.APP_SECRET) {
  loadOpenClawConfig();
}

module.exports = { CONFIG };


path: scripts/create_bitable_app.js
content: /**
 * 创建全新的飞书多维表格
 * 用法: node scripts/create_bitable_app.js --name "表格名称"
 */
const path = require('path');
const { apiPost } = require(path.join(__dirname, 'request'));
const { CONFIG } = require(path.join(__dirname, 'config'));
const readline = require('readline');

async function createBitableApp(name) {
  console.log(`🔄 正在创建多维表格: ${name}...`);
  
  const result = await apiPost('/bitable/v1/apps', {
    name: name
  });
  
  if (result.code === 0) {
    const app = result.data.app;
    console.log(`✅ 创建成功！`);
    console.log(`   App [REDACTED_SECRET]`);
    console.log(`   表格名称: ${app.name}`);
    console.log(`   Default Table ID: ${app.default_table_id}`);
    console.log(`   URL: ${app.url}`);
    console.log(`\n💡 下一步操作:`);
    console.log(`   1. 设置配置: node scripts/set_config.js --app-token "${app.app_token}" --table-id "${app.default_table_id}"`);
    console.log(`   2. 初始化字段: node scripts/init_table.js --open-id "[USER_OPEN_ID]"`);
  } else {
    console.log(`❌ 创建失败: ${result.msg}`);
  }
  return result;
}

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

const name = process.argv.find(arg => arg.startsWith('--name='))?.split('=')[1];

if (!name) {
  rl.question('请输入表格名称: ', async (answer) => {
    await createBitableApp(answer.trim());
    rl.close();
  });
} else {
  createBitableApp(name).then(() => process.exit(0));
}


path: scripts/health_check.js
content: /**
 * 飞书工作台账健康检查脚本
 * 用法: node health_check.js [--lang zh|en]
 * 
 * 诊断维度：
 * 1. 网络连通性（是否能访问飞书 API）
 * 2. API 凭证有效性（App ID / App Secret 是否正确）
 * 3. 多维表格可访问性（表格是否存在且有权限）
 * 4. 字段配置完整性（必填字段是否齐全）
 * 5. Token 刷新机制验证
 * 
 * 触发场景：
 * - 用户说"台账好像不工作了"
 * - 用户说"数据没更新"
 * - 用户说"操作没反应"
 * - 英文环境: "worklist not working", "data not updated", "no response"
 * 
 * 使用 request.js 统一请求（带重试/超时/401自动刷新）
 */

const axios = require('axios');
const { getAccessToken, fetchWithRetry } = require('./request');
const { CONFIG } = require('./config');
const { getLanguage, getTemplates, format } = require('./i18n');

// 健康检查项
const CHECKS = {
  NETWORK: 'network',
  API_CREDENTIALS: 'api_credentials',
  TOKEN: 'token',
  TABLE_ACCESS: 'table_access',
  FIELD_CONFIG: 'field_config'
};

const CHECK_NAMES_ZH = {
  [CHECKS.NETWORK]: '网络连通性',
  [CHECKS.API_CREDENTIALS]: 'API 凭证',
  [CHECKS.TOKEN]: 'Token 获取',
  [CHECKS.TABLE_ACCESS]: '表格访问',
  [CHECKS.FIELD_CONFIG]: '字段配置'
};

const CHECK_NAMES_EN = {
  [CHECKS.NETWORK]: 'Network',
  [CHECKS.API_CREDENTIALS]: 'API Credentials',
  [CHECKS.TOKEN]: 'Token',
  [CHECKS.TABLE_ACCESS]: 'Table Access',
  [CHECKS.FIELD_CONFIG]: 'Field Config'
};

// 必填字段（中英文）
const REQUIRED_FIELDS_ZH = ['任务名称', '来源分类', '优先级', '状态', '截止日期'];
const REQUIRED_FIELDS_EN = ['Task Name', 'Source Category', 'Priority', 'Status', 'Deadline'];

function getRequiredFields(lang) {
  return lang === 'en' ? REQUIRED_FIELDS_EN : REQUIRED_FIELDS_ZH;
}

function getCheckName(check, lang) {
  const map = lang === 'en' ? CHECK_NAMES_EN : CHECK_NAMES_ZH;
  return map[check] || check;
}

// 检查 1: 网络连通性
async function checkNetwork() {
  try {
    const start = Date.now();
    await axios.get('https://open.feishu.cn', { timeout: 5000 });
    const latency = Date.now() - start;
    return { status: 'ok', latency, message: `${latency}ms` };
  } catch (error) {
    return { status: 'fail', message: error.code || 'ECONNREFUSED' };
  }
}

// 检查 2: API 凭证格式
function checkApiCredentials() {
  const issues = [];
  
  if (!CONFIG.APP_ID || CONFIG.APP_ID === 'YOUR_APP_ID') {
    issues.push('APP_ID 未配置或为占位符');
  } else if (!CONFIG.APP_ID.startsWith('cli_')) {
    issues.push('APP_ID 格式可能不正确（应为 cli_ 开头）');
  }
  
  if (!CONFIG.APP_SECRET || CONFIG.APP_SECRET === 'YOUR_APP_SECRET') {
    issues.push('APP_SECRET 未配置或为占位符');
  }
  
  if (!CONFIG.APP_TOKEN || CONFIG.APP_TOKEN === 'YOUR_APP_TOKEN') {
    issues.push('APP_TOKEN 未配置或为占位符');
  }
  
  if (!CONFIG.TABLE_ID || CONFIG.TABLE_ID === 'YOUR_TABLE_ID') {
    issues.push('TABLE_ID 未配置或为占位符');
  }
  
  if (issues.length === 0) {
    return { status: 'ok', message: '凭证格式正确' };
  }
  return { status: 'fail', message: issues.join('; ') };
}

// 检查 3: Token 获取
async function checkToken() {
  try {
    // 使用 fetchWithRetry 包装，增加网络抖动容错
    const response = await fetchWithRetry(async () => {
      return axios.post(
        'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
        { app_id: CONFIG.APP_ID, app_secret: CONFIG.APP_SECRET },
        { timeout: 10000 }
      );
    }, 2, [1000, 2000]);  // 2次重试，1s/2s退避
    
    if (response.data.tenant_access_token) {
      return { status: 'ok', message: 'Token 获取成功', [REDACTED_SECRET] };
    }
    
    if (response.data.code === 99991663) {
      return { status: 'fail', message: 'App ID 或 App Secret 错误' };
    }
    
    return { status: 'fail', message: response.data.msg || '未知错误' };
  } catch (error) {
    if (error.response?.data?.code === 99991663) {
      return { status: 'fail', message: 'App ID 或 App Secret 错误（凭证无效）' };
    }
    return { status: 'fail', message: error.message };
  }
}

// 检查 4: 表格访问权限
async function checkTableAccess(token) {
  try {
    const response = await axios.get(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables`,
      { headers: { Authorization: `Bearer ${token}` }, timeout: 10000 }
    );
    
    if (response.data.code === 0) {
      const tables = response.data.data?.items || [];
      const targetTable = tables.find(t => t.table_id === CONFIG.TABLE_ID);
      
      if (targetTable) {
        return { status: 'ok', message: `表格存在: ${targetTable.name}` };
      }
      
      return { status: 'fail', message: `TABLE_ID 不存在或无权限访问（找到 ${tables.length} 个表格）` };
    }
    
    if (response.data.code === 230002) {
      return { status: 'fail', message: 'APP_TOKEN 无效或已失效' };
    }
    
    return { status: 'fail', message: response.data.msg || '未知错误' };
  } catch (error) {
    if (error.response?.data?.code === 230002) {
      return { status: 'fail', message: 'APP_TOKEN 无效或已失效' };
    }
    return { status: 'fail', message: error.message };
  }
}

// 检查 5: 字段配置
async function checkFieldConfig(token) {
  try {
    const response = await axios.get(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${CONFIG.TABLE_ID}/fields`,
      { headers: { Authorization: `Bearer ${token}` }, timeout: 10000 }
    );
    
    if (response.data.code !== 0) {
      return { status: 'fail', message: response.data.msg || '获取字段失败' };
    }
    
    const fields = response.data.data?.items || [];
    const fieldNames = fields.map(f => f.field_name);
    const lang = getLanguage();
    const requiredFields = getRequiredFields(lang);
    
    const missingFields = requiredFields.filter(f => !fieldNames.includes(f));
    
    if (missingFields.length === 0) {
      return { status: 'ok', message: `全部 ${requiredFields.length} 个必填字段齐全（共 ${fields.length} 个字段）` };
    }
    
    return { 
      status: 'warn', 
      message: `缺少 ${missingFields.length} 个必填字段: ${missingFields.join(', ')}` 
    };
  } catch (error) {
    return { status: 'fail', message: error.message };
  }
}

// 生成诊断报告
function generateReport(results, lang) {
  const t = getTemplates(lang);
  const checkNames = lang === 'en' ? CHECK_NAMES_EN : CHECK_NAMES_ZH;
  
  console.log(lang === 'zh' ? '\n🔍 健康检查报告\n' : '\n🔍 Health Check Report\n');
  console.log('─'.repeat(40));
  
  for (const [check, result] of Object.entries(results)) {
    const checkName = checkNames[check] || check;
    const icon = result.status === 'ok' ? '✅' : result.status === 'warn' ? '⚠️ ' : '❌';
    const label = lang === 'zh' 
      ? { ok: '正常', fail: '异常', warn: '警告' }[result.status] 
      : { ok: 'OK', fail: 'FAIL', warn: 'WARN' }[result.status];
    
    console.log(`${icon} ${checkName}: [${label}] ${result.message}`);
  }
  
  console.log('─'.repeat(40));
  
  // 汇总诊断
  const allOk = Object.values(results).every(r => r.status === 'ok');
  const failCount = Object.values(results).filter(r => r.status === 'fail').length;
  
  console.log();
  
  if (allOk) {
    console.log(lang === 'zh' 
      ? '✅ 所有检查通过，台账功能正常。可能是操作延迟或数据暂无更新。' 
      : '✅ All checks passed. Worklist is functioning normally. It may be operation delay or no new data.');
  } else if (results[CHECKS.NETWORK]?.status === 'fail') {
    console.log(lang === 'zh' 
      ? '🌐 网络问题：无法访问飞书 API。请检查网络连接。' 
      : '🌐 Network issue: Cannot reach Feishu API. Please check your network connection.');
  } else if (results[CHECKS.API_CREDENTIALS]?.status === 'fail' || results[CHECKS.TOKEN]?.status === 'fail') {
    console.log(lang === 'zh' 
      ? '🔑 凭证问题：API 凭证无效。请检查 config.js 中的 APP_ID / APP_SECRET。' 
      : '🔑 Credential issue: API credentials invalid. Please check APP_ID / APP_SECRET in config.js.');
  } else if (results[CHECKS.TABLE_ACCESS]?.status === 'fail') {
    console.log(lang === 'zh' 
      ? '📋 表格问题：无法访问多维表格。请检查 APP_TOKEN / TABLE_ID 是否正确，或确认表格是否被删除。' 
      : '📋 Table issue: Cannot access bitable. Please check APP_TOKEN / TABLE_ID or verify the table exists.');
  } else if (results[CHECKS.FIELD_CONFIG]?.status !== 'ok') {
    console.log(lang === 'zh' 
      ? '⚙️  配置问题：字段配置不完整。请运行 `node scripts/init_table.js` 补全字段。' 
      : '⚙️  Config issue: Field configuration incomplete. Please run `node scripts/init_table.js` to fix.');
  } else {
    console.log(lang === 'zh' 
      ? '⚠️  存在异常，请根据上述报告定位问题。' 
      : '⚠️  Some issues detected. Please review the report above.');
  }
  
  // 建议操作
  console.log();
  if (lang === 'zh') {
    console.log('💡 常用修复命令：');
    console.log('   node scripts/health_check.js          # 重新诊断');
    console.log('   node scripts/init_table.js            # 初始化/修复字段');
    console.log('   node scripts/set_config.js --list      # 查看当前配置');
    console.log('   node scripts/set_config.js --app-token "xxx" --table-id "xxx"  # 更新配置');
  } else {
    console.log('💡 Common fixes:');
    console.log('   node scripts/health_check.js          # Re-diagnose');
    console.log('   node scripts/init_table.js            # Initialize/fix fields');
    console.log('   node scripts/set_config.js --list      # View current config');
    console.log('   node scripts/set_config.js --app-token "xxx" --table-id "xxx"  # Update config');
  }
  
  return allOk;
}

async function healthCheck() {
  const lang = getLanguage();
  const t = getTemplates(lang);
  
  console.log(lang === 'zh' ? '🔄 正在进行健康检查...' : '🔄 Running health check...');
  
  const results = {};
  
  // 检查 1: 网络
  results[CHECKS.NETWORK] = await checkNetwork();
  console.log(results[CHECKS.NETWORK].status === 'ok' 
    ? `✅ ${getCheckName(CHECKS.NETWORK, lang)}: ${results[CHECKS.NETWORK].message}` 
    : `❌ ${getCheckName(CHECKS.NETWORK, lang)}: ${results[CHECKS.NETWORK].message}`);
  
  // 检查 2: API 凭证
  results[CHECKS.API_CREDENTIALS] = checkApiCredentials();
  console.log(results[CHECKS.API_CREDENTIALS].status === 'ok'
    ? `✅ ${getCheckName(CHECKS.API_CREDENTIALS, lang)}: ${results[CHECKS.API_CREDENTIALS].message}`
    : `❌ ${getCheckName(CHECKS.API_CREDENTIALS, lang)}: ${results[CHECKS.API_CREDENTIALS].message}`);
  
  // 如果凭证格式有问题，后续检查可能无法完成
  if (results[CHECKS.API_CREDENTIALS].status === 'fail') {
    results[CHECKS.TOKEN] = { status: 'skip', message: '跳过（凭证格式问题）' };
    results[CHECKS.TABLE_ACCESS] = { status: 'skip', message: '跳过（凭证格式问题）' };
    results[CHECKS.FIELD_CONFIG] = { status: 'skip', message: '跳过（凭证格式问题）' };
    generateReport(results, lang);
    return;
  }
  
  // 检查 3: Token
  results[CHECKS.TOKEN] = await checkToken();
  console.log(results[CHECKS.TOKEN].status === 'ok'
    ? `✅ ${getCheckName(CHECKS.TOKEN, lang)}: ${results[CHECKS.TOKEN].message}`
    : `❌ ${getCheckName(CHECKS.TOKEN, lang)}: ${results[CHECKS.TOKEN].message}`);
  
  if (results[CHECKS.TOKEN].status === 'fail') {
    results[CHECKS.TABLE_ACCESS] = { status: 'skip', message: '跳过（Token 获取失败）' };
    results[CHECKS.FIELD_CONFIG] = { status: 'skip', message: '跳过（Token 获取失败）' };
    generateReport(results, lang);
    return;
  }
  
  const [REDACTED_SECRET];
  
  // 检查 4: 表格访问
  results[CHECKS.TABLE_ACCESS] = await checkTableAccess(token);
  console.log(results[CHECKS.TABLE_ACCESS].status === 'ok'
    ? `✅ ${getCheckName(CHECKS.TABLE_ACCESS, lang)}: ${results[CHECKS.TABLE_ACCESS].message}`
    : results[CHECKS.TABLE_ACCESS].status === 'skip'
    ? `⏭️  ${getCheckName(CHECKS.TABLE_ACCESS, lang)}: ${results[CHECKS.TABLE_ACCESS].message}`
    : `❌ ${getCheckName(CHECKS.TABLE_ACCESS, lang)}: ${results[CHECKS.TABLE_ACCESS].message}`);
  
  if (results[CHECKS.TABLE_ACCESS].status === 'fail') {
    results[CHECKS.FIELD_CONFIG] = { status: 'skip', message: '跳过（表格访问失败）' };
    generateReport(results, lang);
    return;
  }
  
  // 检查 5: 字段配置
  results[CHECKS.FIELD_CONFIG] = await checkFieldConfig(token);
  console.log(results[CHECKS.FIELD_CONFIG].status === 'ok'
    ? `✅ ${getCheckName(CHECKS.FIELD_CONFIG, lang)}: ${results[CHECKS.FIELD_CONFIG].message}`
    : results[CHECKS.FIELD_CONFIG].status === 'warn'
    ? `⚠️  ${getCheckName(CHECKS.FIELD_CONFIG, lang)}: ${results[CHECKS.FIELD_CONFIG].message}`
    : `❌ ${getCheckName(CHECKS.FIELD_CONFIG, lang)}: ${results[CHECKS.FIELD_CONFIG].message}`);
  
  generateReport(results, lang);
}

async function main() {
  try {
    await healthCheck();
  } catch (error) {
    const lang = getLanguage();
    console.error(lang === 'zh' ? `❌ 健康检查失败: ${error.message}` : `❌ Health check failed: ${error.message}`);
    process.exit(1);
  }
}

main();

path: scripts/i18n.js
content: /**
 * 国际化文本模板
 * 用于定时提醒的文本输出
 */

const I18N = {
  // ==================== 早晨提醒 ====================
  zh: {
    morning: {
      greeting: '早呀～花半小时处理邮件和OA，理清今日待办，以最佳状态开启今天！',
      urgentTitle: '今日重点（紧急且重要 × {count}）：',
      taskItem: '• {name} | 优先级：{priority} | 截止：{deadline}',
      noUrgent: '今日暂无紧急且重要的任务',
      inProgressTitle: '进行中/待办任务（{count} 项）：',
      tableHeader: '任务名 | 状态 | 截止 | 优先级',
      tableRow: '{name} | {status} | {deadline} | {priority}',
      suggestion: '建议时间安排：先抽 30 分钟处理 OA待办审批、工作邮件处理，再补充/更新当日工作清单，之后根据任务紧急程度推进。'
    },
    afternoon: {
      greeting: '下午好～开始前建议先花半小时清一下邮件和待办，理顺思路，下午工作更顺～',
      urgentTitle: '今日重点（紧急且重要 × {count}）：',
      taskItem: '• {name} | 优先级：{priority} | 截止：{deadline}',
      noUrgent: '今日暂无紧急且重要的任务',
      inProgressTitle: '进行中/待办任务（{count} 项）：',
      suggestion: '建议时间安排：先抽 30 分钟处理 OA待办审批、工作邮件处理，再补充/更新当日工作清单，之后根据任务紧急程度推进。'
    },
    review: {
      intro: '下午好！开始今日进度复盘——',
      completionRate: '📊 今日完成率：今日工作清单共 {total} 项（截止日期为今天），完成 {done} 项，完成率 {rate}%。',
      uncompletedTitle: '📋 未完成任务（截止日期为今天）：',
      taskItem: '{index}. {name} — 状态：{status}。处理方式：{action}',
      noUncompleted: '今日已全部完成，棒棒的 🎉',
      obstaclesPrompt: '🔍 阻碍分析：以上未完成任务中，存在哪些问题或阻碍？请告诉我，我来帮你分析并填写记录。',
      tomorrowPreview: '⏰ 明日预告：明天重要且紧急的工作是 {tasks}，建议优先处理。',
      noTomorrow: '明日暂无紧急重要的任务。'
    },
    status: {
      todo: '待办',
      inProgress: '进行中',
      done: '已完成',
      cancelled: '取消'
    },
    priority: {
      urgent: '①紧急且重要',
      important: '②重要不紧急',
      urgentLess: '③紧急不重要',
      normal: '④不紧急不重要'
    },
    source: {
      planned: '计划任务[P]',
      urgent: '临时任务[U]',
      routine: '例行任务[R]',
      collab: '协作任务[C]'
    },
    listTasks: {
      header: '📋 任务列表 (共 {count} 条)：\n',
      empty: '(暂无任务记录)',
      item: '{index}. {name}',
      details: '   来源: {source} | 优先级: {priority} | 状态: {status}',
      deadline: '   截止: {deadline} | 时长: {duration} | 开始: {start}',
      requirement: '   工作要求: {req}',
      stakeholder: '   干系人: {stakeholder}',
      issues: '   存在问题: {issues}',
      notes: '   备注: {notes}',
      id: '   ID: {id}'
    },
    confirm: {
      insertSuccess: '✅ 录入成功！',
      recordId: 'record_id: {id}',
      insertFailed: '❌ 录入失败: {msg}',
      updateSuccess: '✅ 更新成功！',
      updateFailed: '❌ 更新失败: {msg}'
    },
    initTable: {
      checking: '🔄 初始化多维表格...',
      step1: '📋 Step 1: 检查表格...',
      tableNotFound: '⚠️  表格不存在，正在创建...',
      tableCreated: '✅ 表格创建成功，ID: {id}',
      tableCreationFailed: '❌ 表格创建失败: {msg}',
      tableExists: '✅ 表格已存在',
      step2: '📋 Step 2: 检查字段...',
      existingFields: '   已有的字段 ({count}): {fields}',
      step3: '📋 Step 3: 创建缺失的字段...',
      fieldExists: '   ✓ {name} (已存在)',
      fieldCreating: '   + {name} (创建中...)',
      fieldCreated: '   ✅ {name} 创建成功',
      fieldFailed: '   ❌ {name} 创建失败: {msg}',
      reportTitle: '\n📊 初始化完成报告：',
      reportTable: '   表格: {status}, ID: {id}',
      reportNew: '   本次新增字段: {fields}',
      reportFailed: '   失败字段: {fields}',
      manualCreate: '\n💡 请在飞书中手动创建以下字段：',
      complete: '\n✅ 多维表格初始化完成！',
      ready: '   现在可以开始使用任务管理功能了。'
    },
    permission: {
      adding: '🔄 正在为您添加多维表格编辑权限...',
      success: '权限添加成功',
      failed: '权限添加失败',
      error: '权限添加异常',
      closed: '\n✅ 权限配置完成，台账已就绪！'
    }
  },

  en: {
    morning: {
      greeting: 'Good morning! Start with 30 minutes to handle emails and OA, clear your to-do list, and kick off the day right!',
      urgentTitle: '🔥 Today\'s Focus (Urgent & Important × {count}):',
      taskItem: '• {name} | Priority: {priority} | Deadline: {deadline}',
      noUrgent: 'No urgent & important tasks today',
      inProgressTitle: 'In Progress / To Do ({count} items):',
      tableHeader: 'Task | Status | Deadline | Priority',
      tableRow: '{name} | {status} | {deadline} | {priority}',
      suggestion: 'Suggested schedule: Spend 30 min on OA approvals & emails first, then update your work list. Push forward based on urgency.'
    },
    afternoon: {
      greeting: 'Good afternoon! Before diving in, suggest clearing emails and pending tasks for 30 min to get organized～',
      urgentTitle: '🔥 Today\'s Focus (Urgent & Important × {count}):',
      taskItem: '• {name} | Priority: {priority} | Deadline: {deadline}',
      noUrgent: 'No urgent & important tasks today',
      inProgressTitle: 'In Progress / To Do ({count} items):',
      suggestion: 'Suggested schedule: Spend 30 min on OA approvals & emails first, then update your work list. Push forward based on urgency.'
    },
    review: {
      intro: 'Good afternoon! Let\'s start today\'s review——',
      completionRate: '📊 Completion Rate: {total} tasks due today, {done} completed, {rate}% done.',
      uncompletedTitle: '📋 Uncompleted Tasks (due today):',
      taskItem: '{index}. {name} — Status: {status}. Action: {action}',
      noUncompleted: 'All tasks completed today, great job! 🎉',
      obstaclesPrompt: '🔍 Obstacle Analysis: What issues or blockers do you have for the above uncompleted tasks? Let me know and I\'ll log them.',
      tomorrowPreview: '⏰ Tomorrow\'s Focus: {tasks} are urgent & important tomorrow, prioritize them.',
      noTomorrow: 'No urgent tasks tomorrow.'
    },
    status: {
      todo: 'To Do',
      inProgress: 'In Progress',
      done: 'Done',
      cancelled: 'Cancelled'
    },
    priority: {
      urgent: '① Urgent and Important',
      important: '② Not Urgent but Important',
      urgentLess: '③ Urgent but Not Important',
      normal: '④ Not Urgent and Not Important'
    },
    source: {
      planned: 'Planned[P]',
      urgent: 'Urgent[U]',
      routine: 'Routine[R]',
      collab: 'Collab[C]'
    },
    listTasks: {
      header: '📋 Task List ({count} items):\n',
      empty: '(No tasks yet)',
      item: '{index}. {name}',
      details: '   Source: {source} | Priority: {priority} | Status: {status}',
      deadline: '   Deadline: {deadline} | Duration: {duration} | Start: {start}',
      requirement: '   Requirements: {req}',
      stakeholder: '   Stakeholder: {stakeholder}',
      issues: '   Issues: {issues}',
      notes: '   Notes: {notes}',
      id: '   ID: {id}'
    },
    confirm: {
      insertSuccess: '✅ Task added!',
      recordId: 'record_id: {id}',
      insertFailed: '❌ Insert failed: {msg}',
      updateSuccess: '✅ Update successful!',
      updateFailed: '❌ Update failed: {msg}'
    },
    initTable: {
      checking: '🔄 Initializing bitable...',
      step1: '📋 Step 1: Checking table...',
      tableNotFound: '⚠️  Table not found, creating...',
      tableCreated: '✅ Table created, ID: {id}',
      tableCreationFailed: '❌ Table creation failed: {msg}',
      tableExists: '✅ Table exists',
      step2: '📋 Step 2: Checking fields...',
      existingFields: '   Existing fields ({count}): {fields}',
      step3: '📋 Step 3: Creating missing fields...',
      fieldExists: '   ✓ {name} (exists)',
      fieldCreating: '   + {name} (creating...)',
      fieldCreated: '   ✅ {name} created',
      fieldFailed: '   ❌ {name} failed: {msg}',
      reportTitle: '\n📊 Initialization Report:',
      reportTable: '   Table: {status}, ID: {id}',
      reportNew: '   New fields created: {fields}',
      reportFailed: '   Failed fields: {fields}',
      manualCreate: '\n💡 Please create these fields manually in Feishu:',
      complete: '\n✅ Bitable initialization complete!',
      ready: '   You can now start using the task management.'
    },
    permission: {
      adding: '🔄 Adding bitable edit permission for you...',
      success: 'Permission added successfully',
      failed: 'Permission add failed',
      error: 'Permission add error',
      closed: '\n✅ Permission configured. Your worklist is ready!'
    }
  }
};

/**
 * 获取指定语言的文本模板
 * @param {string} lang - 'zh' 或 'en'
 * @returns {object} 文本模板对象
 */
function getTemplates(lang) {
  return I18N[lang] || I18N.zh;
}

/**
 * 获取用户偏好语言
 * @returns {string} 'zh' 或 'en'
 */
function getLanguage() {
  const preferences = require('./preferences');
  const lang = preferences.getLanguage();
  return lang === 'en' ? 'en' : 'zh';
}

/**
 * 格式化模板字符串
 * @param {string} template - 模板字符串
 * @param {object} data - 填充数据
 * @returns {string} 格式化后的字符串
 */
function format(template, data) {
  let result = template;
  for (const key in data) {
    const regex = new RegExp(`\\{${key}\\}`, 'g');
    result = result.replace(regex, data[key]);
  }
  return result;
}

module.exports = { I18N, getTemplates, getLanguage, format };

path: scripts/init_table.js
content: /**
 * 初始化飞书多维表格（自动创建表格和字段，并配置用户权限）
 * 用法: node init_table.js [--lang zh|en] [--open-id USER_OPEN_ID]
 *
 * 功能：
 * 1. 检查多维表格是否存在，不存在则自动创建
 * 2. 检查必填字段是否存在+类型是否匹配，不存在或类型不匹配则创建/更新
 * 3. 配置字段选项（单选字段）
 * 4. 将指定用户添加为表格编辑者（需要 --open-id 参数）
 *
 * 安全策略：
 * - 字段存在且类型一致 → 跳过（不覆盖用户手动配置）
 * - 字段存在但类型不一致 → 提示用户确认后再更新
 * - 字段不存在 → 自动创建
 *
 * 使用 request.js 统一请求（带重试/超时/401自动刷新）
 */

const axios = require('axios');
const { getAccessToken } = require('./request');
const { CONFIG } = require('./config');
const { getLanguage, getTemplates, format } = require('./i18n');

// 尝试导入 addPermission（如果 add_permission.js 存在且可访问）
let addPermission = null;
try {
  const permModule = require('./add_permission');
  addPermission = permModule.addPermission;
} catch (e) {
  // add_permission.js 不存在或导入失败，继续（权限配置为可选项）
}

// 字段定义（中英文）
const FIELDS_ZH = [
  { name: '任务名称', type: 1, description: '任务名称' },
  { name: '来源分类', type: 3, description: '来源分类', options: ['计划任务[P]', '临时任务[U]', '例行任务[R]', '协作任务[C]'] },
  { name: '优先级', type: 3, description: '优先级', options: ['①紧急且重要', '②重要不紧急', '③紧急不重要', '④不紧急不重要'] },
  { name: '状态', type: 3, description: '状态', options: ['待办', '进行中', '已完成', '取消'] },
  { name: '截止日期', type: 5, description: '截止日期' },
  { name: '开始时间', type: 5, description: '开始时间' },
  { name: '工作要求', type: 1, description: '工作要求' },
  { name: '工作链接', type: 15, description: '工作链接' },
  { name: '存在问题', type: 1, description: '存在问题' },
  { name: '干系人', type: 1, description: '干系人' },
  { name: '预计时长', type: 1, description: '预计时长' },
  { name: '备注', type: 2, description: '备注' }
];

const FIELDS_EN = [
  { name: 'Task Name', type: 1, description: 'Task Name' },
  { name: 'Source Category', type: 3, description: 'Source Category', options: ['Planned[P]', 'Urgent[U]', 'Routine[R]', 'Collab[C]'] },
  { name: 'Priority', type: 3, description: 'Priority', options: ['① Urgent and Important', '② Not Urgent but Important', '③ Urgent but Not Important', '④ Not Urgent and Not Important'] },
  { name: 'Status', type: 3, description: 'Status', options: ['To Do', 'In Progress', 'Done', 'Cancelled'] },
  { name: 'Deadline', type: 5, description: 'Deadline' },
  { name: 'Start Time', type: 5, description: 'Start Time' },
  { name: 'Requirements', type: 1, description: 'Requirements' },
  { name: 'Work Link', type: 15, description: 'Work Link' },
  { name: 'Issues', type: 1, description: 'Issues' },
  { name: 'Stakeholder', type: 1, description: 'Stakeholder' },
  { name: 'Estimated Time', type: 1, description: 'Estimated Time' },
  { name: 'Notes', type: 2, description: 'Notes' }
];

// 字段类型映射
const FIELD_TYPE_NAMES = {
  1: 'Text',
  2: 'Multi-line Text',
  3: 'SingleSelect',
  4: 'URL',
  5: 'Date',
  15: 'Attachment'
};

// 获取语言对应的字段定义
function getFields(lang) {
  return lang === 'en' ? FIELDS_EN : FIELDS_ZH;
}

// 检查表格是否存在
async function checkTableExists(token) {
  try {
    const response = await axios.get(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables`,
      { headers: { Authorization: `Bearer ${token}` }, timeout: 10000 }
    );

    if (response.data.code === 0) {
      const tables = response.data.data?.items || [];
      const targetTable = tables.find(t => t.table_id === CONFIG.TABLE_ID);
      return targetTable ? { exists: true, tableId: targetTable.table_id, tableName: targetTable.name } : { exists: false };
    }
    return { exists: false };
  } catch (error) {
    return { exists: false, error: error.response?.data?.msg || error.message };
  }
}

// 创建多维表格
async function createTable(token, name) {
  try {
    const response = await axios.post(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables`,
      { table: { name } },
      { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 10000 }
    );

    if (response.data.code === 0) {
      return { success: true, tableId: response.data.data?.table_id };
    }
    return { success: false, msg: response.data.msg };
  } catch (error) {
    return { success: false, error: error.response?.data?.msg || error.message };
  }
}

// 获取表格字段
async function getTableFields(token, tableId) {
  try {
    const response = await axios.get(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${tableId}/fields`,
      { headers: { Authorization: `Bearer ${token}` }, timeout: 10000 }
    );
    return response.data.code === 0 ? response.data.data?.items || [] : [];
  } catch (error) {
    return [];
  }
}

// 创建字段
async function createField(token, tableId, fieldDef) {
  try {
    const response = await axios.post(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${tableId}/fields`,
      {
        field_name: fieldDef.name,
        type: fieldDef.type,
        property: fieldDef.type === 3 ? { options: fieldDef.options?.map(opt => ({ name: opt })) } : undefined
      },
      { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 10000 }
    );
    return response.data.code === 0 ? { success: true } : { success: false, msg: response.data.msg };
  } catch (error) {
    return { success: false, error: error.response?.data?.msg || error.message };
  }
}

// 删除记录（用于清空新建表格的默认行数据）
async function deleteRecord(token, tableId, recordId) {
  try {
    const response = await axios.delete(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${tableId}/records/${recordId}`,
      { headers: { Authorization: `Bearer ${token}` }, timeout: 10000 }
    );
    return response.data.code === 0 ? { success: true } : { success: false, msg: response.data.msg };
  } catch (error) {
    return { success: false, error: error.response?.data?.msg || error.message };
  }
}

// 删除字段（用于清空新建表格的默认字段）
async function deleteField(token, tableId, fieldId) {
  try {
    const response = await axios.delete(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${tableId}/fields/${fieldId}`,
      { headers: { Authorization: `Bearer ${token}` }, timeout: 10000 }
    );
    return response.data.code === 0 ? { success: true } : { success: false, msg: response.data.msg };
  } catch (error) {
    return { success: false, error: error.response?.data?.msg || error.message };
  }
}

// 更新字段（类型变更）
async function updateField(token, tableId, fieldId, fieldDef) {
  try {
    const response = await axios.put(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${tableId}/fields/${fieldId}`,
      {
        field_name: fieldDef.name,
        type: fieldDef.type,
        property: fieldDef.type === 3 ? { options: fieldDef.options?.map(opt => ({ name: opt })) } : undefined
      },
      { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 10000 }
    );
    return response.data.code === 0 ? { success: true } : { success: false, msg: response.data.msg };
  } catch (error) {
    return { success: false, error: error.response?.data?.msg || error.message };
  }
}

// 更新单选字段选项
async function updateFieldOptions(token, tableId, fieldId, options) {
  try {
    const response = await axios.put(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${tableId}/fields/${fieldId}`,
      {
        type: 3,
        property: { options: options.map(opt => ({ name: opt })) }
      },
      { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 10000 }
    );
    return response.data.code === 0 ? { success: true } : { success: false, msg: response.data.msg };
  } catch (error) {
    return { success: false, error: error.response?.data?.msg || error.message };
  }
}

// 检查单选字段选项是否完整
function checkOptionsComplete(existingOptions, requiredOptions) {
  if (!existingOptions || existingOptions.length === 0) return false;
  const existingNames = existingOptions.map(o => o.name);
  return requiredOptions.every(opt => existingNames.includes(opt));
}

// 解析命令行参数
function parseArgs() {
  const args = process.argv.slice(2);
  let lang = null;
  let openId = null;

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--lang' && args[i + 1]) {
      lang = args[i + 1];
      i++;
    } else if (args[i] === '--open-id' && args[i + 1]) {
      openId = args[i + 1];
      i++;
    }
  }

  return { lang, openId };
}

async function initTable() {
  const { lang: argLang, openId } = parseArgs();
  const lang = argLang || getLanguage();

  // 【强制检查】语言环境必须已设置（通过 --lang 参数或 preferences.json）
  // 如果语言未设置，脚本必须退出并报错，禁止在语言不明的情况下创建表格
  if (!lang) {
    console.error('============================================================');
    console.error('❌ 错误：语言环境未设置');
    console.error('');
    console.error('在创建台账之前，必须先确认语言环境。');
    console.error('');
    console.error('请先告诉 AI 您的语言偏好：');
    console.error('  - 输入 "中文" 或 "c" → 使用中文');
    console.error('  - 输入 "英文" 或 "e" → Use English');
    console.error('');
    console.error('示例：告诉 AI "我想用中文" 或 "I prefer English"');
    console.error('============================================================');
    process.exit(1);
  }

  const t = getTemplates(lang);
  const fields = getFields(lang);

  console.log(t.initTable.checking);
  const token = await getAccessToken();

  // Step 1: 检查表格是否存在
  console.log(t.initTable.step1);
  const tableCheck = await checkTableExists(token);

  let tableId = CONFIG.TABLE_ID;
  let tableCreated = false;

  if (!tableCheck.exists) {
    console.log(t.initTable.tableNotFound);
    const tableName = lang === 'zh' ? '工作台账' : 'Worklist';
    const createResult = await createTable(token, tableName);

    if (createResult.success) {
      tableId = createResult.tableId;
      tableCreated = true;
      console.log(format(t.initTable.tableCreated, { id: tableId }));
    } else {
      console.error(format(t.initTable.tableCreationFailed, { msg: createResult.msg || createResult.error }));
      process.exit(1);
    }
  } else {
    console.log(t.initTable.tableExists);
    if (tableCheck.tableName) {
      console.log(`   Table name: ${tableCheck.tableName}`);
    }
  }

  // Step 1.5: 清空新建表格的默认字段和行数据
  if (tableCreated) {
    console.log(lang === 'zh' ? '   → 清空默认字段...' : '   → Clearing default fields...');
    const defaultFields = await getTableFields(token, tableId);
    for (const field of defaultFields) {
      const delResult = await deleteField(token, tableId, field.field_id);
      if (delResult.success) {
        console.log(lang === 'zh' ? `   ✅ 已删除默认字段: ${field.field_name}` : `   ✅ Deleted default field: ${field.field_name}`);
      } else {
        console.log(lang === 'zh' ? `   ⚠️ 删除字段失败: ${field.field_name} - ${delResult.msg || delResult.error}` : `   ⚠️ Failed to delete: ${field.field_name} - ${delResult.msg || delResult.error}`);
      }
    }

    console.log(lang === 'zh' ? '   → 清空默认行数据...' : '   → Clearing default records...');
    const recordsResponse = await axios.get(
      `https://open.feishu.cn/open-apis/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${tableId}/records?page_size=500`,
      { headers: { Authorization: `Bearer ${token}` }, timeout: 10000 }
    );
    const records = recordsResponse.data?.data?.items || [];
    let deletedCount = 0;
    for (const record of records) {
      const delResult = await deleteRecord(token, tableId, record.record_id);
      if (delResult.success) deletedCount++;
    }
    console.log(lang === 'zh' ? `   ✅ 已删除 ${deletedCount} 条默认记录` : `   ✅ Deleted ${deletedCount} default records`);
  }

  // Step 2: 获取现有字段
  console.log(t.initTable.step2);
  const existingFields = await getTableFields(token, tableId);
  const existingFieldMap = {};
  existingFields.forEach(f => { existingFieldMap[f.field_name] = f; });

  console.log(format(t.initTable.existingFields, {
    count: existingFields.length,
    fields: existingFields.map(f => f.field_name).join(', ')
  }));

  // Step 3: 创建/更新缺失的字段
  console.log(t.initTable.step3);

  const createdFields = [];
  const updatedFields = [];
  const skippedFields = [];
  const failedFields = [];

  for (const fieldDef of fields) {
    const existingField = existingFieldMap[fieldDef.name];

    if (existingField) {
      // 字段已存在，检查类型
      if (existingField.type === fieldDef.type) {
        // 类型一致 → 跳过（安全策略：不覆盖用户手动配置）
        console.log(format(t.initTable.fieldExists, { name: fieldDef.name }));
        skippedFields.push(fieldDef.name);

        // 检查单选字段选项是否完整
        if (fieldDef.type === 3 && fieldDef.options && fieldDef.options.length > 0) {
          const existingOptions = existingField.property?.options || [];
          if (!checkOptionsComplete(existingOptions, fieldDef.options)) {
            console.log(lang === 'zh' ? `   → 更新选项...` : `   → Updating options...`);
            await updateFieldOptions(token, tableId, existingField.field_id, fieldDef.options);
          }
        }
      } else {
        // 类型不一致 → 提示并询问（这里简化处理，直接更新但记录）
        console.log(lang === 'zh'
          ? `   ⚠️  ${fieldDef.name} 类型不匹配 (当前: ${FIELD_TYPE_NAMES[existingField.type] || existingField.type}, 需要: ${FIELD_TYPE_NAMES[fieldDef.type]}) → 更新中...`
          : `   ⚠️  ${fieldDef.name} type mismatch (current: ${FIELD_TYPE_NAMES[existingField.type] || existingField.type}, needed: ${FIELD_TYPE_NAMES[fieldDef.type]}) → updating...`
        );
        const updateResult = await updateField(token, tableId, existingField.field_id, fieldDef);
        if (updateResult.success) {
          updatedFields.push(fieldDef.name);
          console.log(lang === 'zh' ? `   ✅ ${fieldDef.name} 更新成功` : `   ✅ ${fieldDef.name} updated`);
        } else {
          failedFields.push({ name: fieldDef.name, msg: updateResult.msg || updateResult.error });
          console.error(lang === 'zh' ? `   ❌ ${fieldDef.name} 更新失败: ${updateResult.msg || updateResult.error}` : `   ❌ ${fieldDef.name} update failed: ${updateResult.msg || updateResult.error}`);
        }
      }
    } else {
      // 字段不存在 → 创建
      console.log(format(t.initTable.fieldCreating, { name: fieldDef.name }));
      const result = await createField(token, tableId, fieldDef);

      if (result.success) {
        createdFields.push(fieldDef.name);
        console.log(format(t.initTable.fieldCreated, { name: fieldDef.name }));
      } else {
        failedFields.push({ name: fieldDef.name, msg: result.msg || result.error });
        console.error(format(t.initTable.fieldFailed, { name: fieldDef.name, msg: result.msg || result.error }));
      }
    }
  }

  // Step 4: 汇总报告
  console.log(t.initTable.reportTitle);
  console.log(format(t.initTable.reportTable, {
    status: tableCreated ? (lang === 'zh' ? '新建' : 'Created') : (lang === 'zh' ? '已存在' : 'Existing'),
    id: tableId
  }));

  const newFieldsStr = createdFields.length > 0 ? createdFields.join(', ') : (lang === 'zh' ? '无' : 'None');
  const updatedFieldsStr = updatedFields.length > 0 ? updatedFields.join(', ') : (lang === 'zh' ? '无' : 'None');
  const skippedFieldsStr = skippedFields.length > 0 ? skippedFields.join(', ') : (lang === 'zh' ? '无' : 'None');
  const failedFieldsStr = failedFields.length > 0 ? failedFields.map(f => f.name).join(', ') : (lang === 'zh' ? '无' : 'None');

  console.log(lang === 'zh' ? `   新增字段: ${newFieldsStr}` : `   New fields: ${newFieldsStr}`);
  console.log(lang === 'zh' ? `   更新字段: ${updatedFieldsStr}` : `   Updated fields: ${updatedFieldsStr}`);
  console.log(lang === 'zh' ? `   跳过字段(类型一致): ${skippedFieldsStr}` : `   Skipped (type match): ${skippedFieldsStr}`);
  console.log(lang === 'zh' ? `   失败字段: ${failedFieldsStr}` : `   Failed fields: ${failedFieldsStr}`);

  if (failedFields.length > 0) {
    console.log(t.initTable.manualCreate);
    failedFields.forEach(f => console.log(`   - ${f.name}`));
    process.exit(1);
  }

  // Step 5: 配置用户权限（如果提供了 --open-id）
  if (openId && addPermission) {
    console.log(lang === 'zh' ? `\n   → 配置用户编辑权限...` : `\n   → Configuring user edit permission...`);
    const permResult = await addPermission(openId, 'edit');
    if (permResult.success) {
      console.log(lang === 'zh' ? `   ✅ 权限配置成功` : `   ✅ Permission configured`);
    } else {
      console.log(lang === 'zh' ? `   ⚠️ 权限配置失败: ${permResult.msg || permResult.error}` : `   ⚠️ Permission failed: ${permResult.msg || permResult.error}`);
      console.log(lang === 'zh' ? `   → 请手动在飞书中添加编辑权限` : `   → Please add permission manually in Feishu`);
    }
  } else if (openId && !addPermission) {
    console.log(lang === 'zh' ? `\n   ⚠️  add_permission.js 未找到，跳过权限配置` : `\n   ⚠️  add_permission.js not found, skipping permission`);
  } else {
    console.log(lang === 'zh' ? `\n   ℹ️  未指定 --open-id，跳过权限配置（如需配置请添加 --open-id USER_OPEN_ID）` : `\n   ℹ️  No --open-id provided, skipping permission (add --open-id USER_OPEN_ID to configure)`);
  }

  console.log(t.initTable.complete);
  console.log(t.initTable.ready);
}

async function main() {
  try {
    await initTable();
  } catch (error) {
    const lang = getLanguage();
    console.error(lang === 'zh' ? `❌ 初始化失败: ${error.message}` : `❌ Initialization failed: ${error.message}`);
    process.exit(1);
  }
}

main();

path: scripts/list_tasks.js
content: /**
 * 读取飞书多维表格任务列表
 * 用法: node list_tasks.js
 * 
 * 根据用户语言偏好，输出格式化后的任务列表
 */

const { apiGet } = require('./request');
const { CONFIG } = require('./config');
const { getLanguage, getTemplates, format } = require('./i18n');

// 中文字段 → 英文字段映射（用于英文模式）
const FIELD_MAP_ZH_TO_EN = {
  '任务名称': 'Task Name',
  '来源分类': 'Source Category',
  '优先级': 'Priority',
  '状态': 'Status',
  '截止日期': 'Deadline',
  '预计时长': 'Estimated Time',
  '开始时间': 'Start Time',
  '工作链接': 'Work Link',
  '工作要求': 'Requirements',
  '干系人': 'Stakeholder',
  '存在问题': 'Issues',
  '备注': 'Notes'
};

// 中文状态值 → 英文状态值映射
const STATUS_MAP = {
  '待办': 'To Do',
  '进行中': 'In Progress',
  '已完成': 'Done',
  '取消': 'Cancelled'
};

// 中文优先级值 → 英文优先级值映射
const PRIORITY_MAP = {
  '①紧急且重要': '① Urgent and Important',
  '②重要不紧急': '② Not Urgent but Important',
  '③紧急不重要': '③ Urgent but Not Important',
  '④不紧急不重要': '④ Not Urgent and Not Important'
};

function translateFieldValue(key, value, lang) {
  if (!value) return value;
  
  if (lang === 'en') {
    if (key === '状态' && STATUS_MAP[value]) return STATUS_MAP[value];
    if (key === '优先级' && PRIORITY_MAP[value]) return PRIORITY_MAP[value];
  }
  
  return value;
}

function formatDate(dateStr, lang) {
  if (!dateStr) return '-';
  const date = new Date(dateStr);
  if (lang === 'zh') {
    return date.toLocaleDateString('zh-CN');
  } else {
    return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
  }
}

async function main() {
  try {
    const lang = getLanguage();
    const t = getTemplates(lang);

    console.log(lang === 'zh' ? '🔄 获取任务列表...' : '🔄 Fetching task list...');
    
    const result = await apiGet(`/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${CONFIG.TABLE_ID}/records`);
    
    if (result.code === 0) {
      const items = result.data?.items || [];
      const count = items.length;
      
      if (lang === 'zh') {
        console.log(`\n📋 任务列表 (共 ${count} 条):\n`);
      } else {
        console.log(`\n${format(t.listTasks.header, { count })}`);
      }
      
      if (items.length === 0) {
        console.log(`   ${t.listTasks.empty}`);
      }
      
      items.forEach((item, index) => {
        const f = item.fields;
        const name = f['任务名称'] || f['Task Name'] || '(无标题)' || '(No title)';
        const source = translateFieldValue('来源分类', f['来源分类'] || f['Source Category'], lang);
        const priority = translateFieldValue('优先级', f['优先级'] || f['Priority'], lang);
        const status = translateFieldValue('状态', f['状态'] || f['Status'], lang);
        const deadline = formatDate(f['截止日期'] || f['Deadline'], lang);
        const duration = f['预计时长'] || f['Estimated Time'] || '-';
        const start = f['开始时间'] || f['Start Time'] || '-';
        const requirement = f['工作要求'] || f['Requirements'] || '';
        const stakeholder = f['干系人'] || f['Stakeholder'] || '';
        const issues = f['存在问题'] || f['Issues'] || '';
        const notes = f['备注'] || f['Notes'] || '';

        if (lang === 'zh') {
          console.log(`${index + 1}. ${name}`);
          console.log(`   来源: ${source} | 优先级: ${priority} | 状态: ${status}`);
          console.log(`   截止: ${deadline} | 时长: ${duration} | 开始: ${start}`);
          if (requirement) console.log(`   工作要求: ${requirement}`);
          if (stakeholder) console.log(`   干系人: ${stakeholder}`);
          if (issues) console.log(`   存在问题: ${issues}`);
          if (notes) console.log(`   备注: ${notes}`);
          console.log(`   ID: ${item.record_id}\n`);
        } else {
          console.log(format(t.listTasks.item, { index: index + 1, name }));
          console.log(format(t.listTasks.details, { source, priority, status }));
          console.log(format(t.listTasks.deadline, { deadline, duration, start }));
          if (requirement) console.log(format(t.listTasks.requirement, { req: requirement }));
          if (stakeholder) console.log(format(t.listTasks.stakeholder, { stakeholder }));
          if (issues) console.log(format(t.listTasks.issues, { issues }));
          if (notes) console.log(format(t.listTasks.notes, { notes }));
          console.log(format(t.listTasks.id, { id: item.record_id }) + '\n');
        }
      });
    } else {
      const errMsg = lang === 'zh' ? `❌ 获取失败: ${result.msg}` : `❌ Fetch failed: ${result.msg}`;
      console.error(errMsg);
      process.exit(1);
    }
  } catch (error) {
    const lang = getLanguage();
    const errMsg = lang === 'zh' ? `❌ 错误: ${error.response?.data?.msg || error.message}` : `❌ Error: ${error.response?.data?.msg || error.message}`;
    console.error(errMsg);
    process.exit(1);
  }
}

main();

path: scripts/preferences.js
content: /**
 * 用户偏好存储工具
 * 
 * 安全机制：
 * - 原子写入：先写 .tmp 文件再 rename，避免读到半文件
 * - 读时合并：读取当前数据后再写入，保留其他字段
 * 
 * 注意：当前为单用户设计，rename 组合在大多数场景足够安全
 *       若未来需多进程并发写入，需引入 lockfile npm 包
 */

const fs = require('fs');
const path = require('path');

// 偏好文件路径（相对于 workspace root）
const PREFERENCES_FILE = path.join(__dirname, '..', 'memory', 'preferences.json');
const TEMP_FILE = PREFERENCES_FILE + '.tmp';

/**
 * 读取用户偏好
 * @returns {object} 用户偏好数据
 */
function getPreferences() {
  try {
    if (fs.existsSync(PREFERENCES_FILE)) {
      const data = fs.readFileSync(PREFERENCES_FILE, 'utf8');
      return JSON.parse(data);
    }
  } catch (e) {
    console.error('[preferences] 读取偏好失败:', e.message);
  }
  return { language: null, lastUpdated: null };
}

/**
 * 保存用户偏好（原子写入）
 * @param {object} preferences 要保存的偏好数据
 * @returns {boolean} 是否保存成功
 */
function savePreferences(preferences) {
  try {
    // Step 1: 确保目录存在
    const memoryDir = path.dirname(PREFERENCES_FILE);
    if (!fs.existsSync(memoryDir)) {
      fs.mkdirSync(memoryDir, { recursive: true });
    }

    // Step 2: 读取当前数据（用于合并）
    let current = { language: null, lastUpdated: null };
    if (fs.existsSync(PREFERENCES_FILE)) {
      try {
        const raw = fs.readFileSync(PREFERENCES_FILE, 'utf8');
        current = JSON.parse(raw);
      } catch (e) {
        // 解析失败，使用空对象继续
      }
    }

    // Step 3: 合并更新（只覆盖 language 和 lastUpdated，保留其他字段）
    const updated = {
      ...current,
      ...preferences,
      lastUpdated: new Date().toISOString()
    };

    // Step 4: 原子写入（先写临时文件再 rename）
    const content = JSON.stringify(updated, null, 2);
    
    // 写临时文件
    fs.writeFileSync(TEMP_FILE, content, 'utf8');
    
    // 原子替换（OS 保证 rename 是原子的）
    fs.renameSync(TEMP_FILE, PREFERENCES_FILE);
    
    return true;
  } catch (e) {
    console.error('[preferences] 保存偏好失败:', e.message);
    
    // 清理可能残留的临时文件
    try {
      if (fs.existsSync(TEMP_FILE)) {
        fs.unlinkSync(TEMP_FILE);
      }
    } catch (cleanupError) {
      // 忽略清理失败
    }
    
    return false;
  }
}

/**
 * 获取语言偏好（中文/英文）
 * @returns {string|null} 'zh' / 'en' / null（未设置）
 */
function getLanguage() {
  const prefs = getPreferences();
  return prefs.language || null;
}

/**
 * 设置语言偏好
 * @param {string} lang 'zh' 或 'en'
 * @returns {boolean} 是否设置成功
 */
function setLanguage(lang) {
  if (lang !== 'zh' && lang !== 'en') {
    return false;
  }
  const prefs = getPreferences();
  prefs.language = lang;
  return savePreferences(prefs);
}

/**
 * 检查目录/文件权限是否正常
 * @returns {object} { writable: boolean, reason: string }
 */
function checkPermissions() {
  const memoryDir = path.dirname(PREFERENCES_FILE);
  
  // 检查目录是否存在
  if (!fs.existsSync(memoryDir)) {
    // 尝试创建
    try {
      fs.mkdirSync(memoryDir, { recursive: true });
      return { writable: true, reason: '目录已创建' };
    } catch (e) {
      return { writable: false, reason: `无法创建目录: ${e.message}` };
    }
  }
  
  // 检查目录是否可写
  try {
    const testFile = path.join(memoryDir, '.write_test');
    fs.writeFileSync(testFile, 'test');
    fs.unlinkSync(testFile);
    return { writable: true, reason: '目录可写' };
  } catch (e) {
    return { writable: false, reason: `目录不可写: ${e.message}` };
  }
}

module.exports = {
  getPreferences,
  savePreferences,
  getLanguage,
  setLanguage,
  checkPermissions
};

path: scripts/query_today_tasks.js
content: /**
 * 查询截止日期为今天且状态<>已完成的任务
 */
const { apiGet } = require('./request');
const { CONFIG } = require('./config');

async function main() {
  const result = await apiGet(`/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${CONFIG.TABLE_ID}/records?page_size=100`);
  
  if (result.code === 0) {
    const items = result.data?.items || [];
    console.log('Total records:', items.length);
    
    // Filter for today's date and status != 已完成
    // 动态获取今天日期（上海时区）
    // 飞书日期字段存储为毫秒时间戳（如上海 2026-05-14 00:00:00 存为 XXXXXXXXX000 = UTC 2026-05-13T16:00:00Z）
    // 注意：历史数据曾错误存储为秒（XXXXXXXXX），需自动识别：值>1e10则为秒，需转毫秒
    const now = new Date();
    const shanghaiTime = new Date(now.getTime() + 8 * 3600000);
    const today = shanghaiTime.toISOString().split('T')[0]; // 上海时区的今天（如 2026-05-14）
    const todayTasks = items.filter(item => {
      const deadline = item.fields['截止日期'];
      const status = item.fields['状态'];
      if (!deadline) return false;
      // 兼容历史错误（秒）和正确数据（毫秒）：>1e10 则为秒转毫秒
      const deadlineMs = deadline > 1e10 ? deadline : deadline * 1000;
      const deadlineStr = new Date(deadlineMs + 8 * 3600000).toISOString().split('T')[0];
      return deadlineStr === today && status !== '已完成';
    });
    
    console.log('Today deadline, not completed:', todayTasks.length);
    todayTasks.forEach((item, i) => {
      console.log(`\n--- Task ${i+1} ---`);
      console.log('ID:', item.record_id);
      console.log('Name:', item.fields['任务名称']);
      console.log('Status:', item.fields['状态']);
      console.log('Priority:', item.fields['优先级']);
      console.log('Deadline:', item.fields['截止日期']);
      console.log('Issues:', item.fields['存在问题']);
      console.log('Notes:', item.fields['备注']);
    });
    
    // Also get completed tasks for today
    const completedToday = items.filter(item => {
      const deadline = item.fields['截止日期'];
      const status = item.fields['状态'];
      if (!deadline) return false;
      const deadlineMs = deadline > 1e10 ? deadline : deadline * 1000;
      const deadlineStr = new Date(deadlineMs + 8 * 3600000).toISOString().split('T')[0];
      return deadlineStr === today && status === '已完成';
    });
    console.log('\n=== Completed Today ===');
    console.log('Completed today:', completedToday.length);
    
  } else {
    console.log('Error:', result.msg);
  }
}

main().catch(console.error);


path: scripts/request.js
content: /**
 * 统一 API 请求工具
 * 
 * 功能：
 * - Token 缓存 + 提前5分钟自动刷新
 * - 401 自动刷新 + 一次重试
 * - 3次指数退避重试（网络错误/5xx）
 * - 10秒超时控制
 * 
 * 使用方式：
 * const { apiGet, apiPost, apiPut } = require('./request');
 */

const axios = require('axios');
const { CONFIG } = require('./config');

// ==================== Token 缓存 ====================
let tokenCache = {
  token: null,
  expireAt: null  // 提前5分钟过期的时间戳
};

// 从 config 读取凭证
function getCredentials() {
  return {
    app_id: CONFIG.APP_ID,
    app_secret: CONFIG.APP_SECRET
  };
}

// 获取 Token（带缓存 + 提前刷新）
async function getAccessToken() {
  const now = Date.now();
  
  // 缓存有效（未到过期时间）→ 直接返回
  if (tokenCache.token && tokenCache.expireAt && now < tokenCache.expireAt) {
    return tokenCache.token;
  }
  
  // 需要刷新
  const credentials = getCredentials();
  if (!credentials.app_id || !credentials.app_secret) {
    throw new Error('APP_ID 或 APP_SECRET 未配置');
  }
  
  try {
    const response = await axios.post(
      'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
      {
        app_id: credentials.app_id,
        app_secret: credentials.app_secret
      },
      { timeout: 10000 }
    );
    
    if (response.data.tenant_access_token) {
      // expire_in 单位是秒，默认7200（2小时）
      const expireIn = response.data.expire_in || 7200;
      // 提前5分钟过期（如实际2小时，则1小时55分后刷新）
      tokenCache.[REDACTED_SECRET];
      tokenCache.expireAt = now + (expireIn * 1000) - 300000;
      
      return tokenCache.token;
    }
    
    throw new Error(response.data.msg || '获取 Token 失败');
  } catch (error) {
    // 清空缓存，下次重新尝试
    tokenCache.token = null;
    tokenCache.expireAt = null;
    throw error;
  }
}

// 强制刷新 Token（用于 401 后）
function forceRefreshToken() {
  tokenCache.token = null;
  tokenCache.expireAt = null;
}

// ==================== sleep 工具 ====================
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// ==================== 重试逻辑 ====================
const MAX_RETRIES = 3;
const RETRY_DELAYS = [1000, 2000, 4000];  // 1s, 2s, 4s 指数退避

// 判断是否可重试（网络错误或5xx）
function isRetryableError(error) {
  // 无响应（网络问题）
  if (!error.response) return true;
  // 5xx 服务器错误
  if (error.response.status >= 500) return true;
  // 429 Too Many Requests（频率限制）
  if (error.response.status === 429) return true;
  return false;
}

// 带重试的请求
async function fetchWithRetry(fn, retries = MAX_RETRIES, delays = RETRY_DELAYS) {
  let lastError;
  
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      // 401 不重试，交给外层处理
      if (error.response?.status === 401) {
        throw error;
      }
      
      // 非可重试错误，直接抛出
      if (!isRetryableError(error)) {
        throw error;
      }
      
      // 还有重试机会
      if (i < retries - 1) {
        await sleep(delays[i]);
        continue;
      }
    }
  }
  
  throw lastError;
}

// ==================== 统一请求封装 ====================
async function apiRequest(method, path, data = null, options = {}) {
  const token = await getAccessToken();
  const url = `https://open.feishu.cn/open-apis${path}`;
  
  const requestFn = async () => {
    const config = {
      method,
      url,
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      timeout: 10000,
      ...options
    };
    
    if (data) {
      config.data = data;
    }
    
    return axios(config);
  };
  
  try {
    const response = await fetchWithRetry(requestFn);
    return response.data;
  } catch (error) {
    // 401 错误：强制刷新 Token 后重试一次
    if (error.response?.status === 401) {
      forceRefreshToken();
      const newToken = await getAccessToken();
      
      const retryFn = async () => {
        const config = {
          method,
          url,
          headers: {
            Authorization: `Bearer ${newToken}`,
            'Content-Type': 'application/json'
          },
          timeout: 10000,
          ...options
        };
        if (data) config.data = data;
        return axios(config);
      };
      
      const retryResponse = await retryFn();
      return retryResponse.data;
    }
    
    // 其他错误
    throw error;
  }
}

// ==================== 对外 API ====================
async function apiGet(path, options = {}) {
  return apiRequest('GET', path, null, options);
}

async function apiPost(path, data, options = {}) {
  return apiRequest('POST', path, data, options);
}

async function apiPut(path, data, options = {}) {
  return apiRequest('PUT', path, data, options);
}

async function apiDelete(path, options = {}) {
  return apiRequest('DELETE', path, null, options);
}

// ==================== 初始化检查 ====================
async function checkConnection() {
  try {
    const token = await getAccessToken();
    return { success: true, token };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

module.exports = {
  getAccessToken,
  forceRefreshToken,
  apiGet,
  apiPost,
  apiPut,
  apiDelete,
  checkConnection,
  fetchWithRetry
};

path: scripts/setup_reminders.js
content: /**
 * setup_reminders.js - 定时提醒设置脚本
 *
 * 功能：
 * 1. 生成三条定时提醒的 cron 配置
 * 2. 直接通过 exec 执行 openclaw cron add 命令（无需用户手动执行）
 *
 * 使用场景：
 * - 用户首次通过 AI 成功录入第一条任务后，AI 自动调用本脚本
 * - 由 AI 直接执行，用户无感知
 *
 * 用法：node setup_reminders.js [--lang zh|en] [--open-id USER_OPEN_ID]
 *   --lang: 指定输出语言，默认读取 memory/preferences.json
 *   --open-id: 可选，传入时同时配置用户 ID（预留字段，当前未使用）
 */

const { execSync } = require('child_process');
const { getLanguage } = require('./preferences');
const path = require('path');
const fs = require('fs');

// 三条定时提醒的配置（Prompt 优化版本 - 2026-05-12）
const REMINDERS = [
  {
    name: '工作日早晨提醒',
    cron: '28 8 * * *',
    tz: 'Asia/Shanghai',
    description_zh: '每天 08:28 发送当日工作重点（优先级/进行中任务）',
    description_en: 'Daily at 08:28 - sends morning work focus (priority tasks + in-progress)',
    message_zh: `早呀~今日工作提醒如下：

【工作日判断】
调用 http://api.jiejiariapi.com/v1/holidays/2026 → 查 isOffDay(false=工作日/调休) → 是则继续，否则静默

【飞书多维表格查询】（APP_TOKEN: YOUR_APP_TOKEN / TABLE_ID: YOUR_TABLE_ID）
- 截止日期为今天且状态<>已完成且状态<>取消的任务（视为今日待办）
- 截止日期为今天且状态=已完成的任务（今日已完成）

【输出格式】
1. 今日完成(X项)
2. 今日重点（优先级①的任务，注明任务名和截止时间）
3. 进行中/待办任务（任务名、状态、截止日期、优先级）
4. 建议时间安排（先处理邮件和OA审批）`,
    message_en: `Good morning! Here's today's work reminder:

【Workday Check】
Call http://api.jiejiariapi.com/v1/holidays/2026 → check isOffDay(false=workday/adjustment) → continue if yes, otherwise stay silent

【Feishu Bitable Query】（APP_TOKEN: YOUR_APP_TOKEN / TABLE_ID: YOUR_TABLE_ID）
- Completed: deadline = today AND status = Done
- Incomplete: start time <= today AND status <> Done AND status <> Cancelled

【Output Format】
1. Completed today (X items)
2. Priority tasks (priority ①, name + deadline)
3. In-progress / To-do tasks (name, status, deadline, priority)
4. Suggested time allocation (process emails and OA first)`,
  },
  {
    name: '工作日下午提醒',
    cron: '0 14 * * *',
    tz: 'Asia/Shanghai',
    description_zh: '每天 14:00 发送下午工作安排（上午未完成任务回顾）',
    description_en: 'Daily at 14:00 - sends afternoon work arrangement (morning incomplete tasks review)',
    message_zh: `下午好~开始前建议先花半小时清一下邮件和待办，理顺思路，下午工作更顺~

【工作日判断】
调用 http://api.jiejiariapi.com/v1/holidays/2026 → 查 isOffDay(false=工作日/调休) → 是则继续，否则静默

【飞书多维表格查询】（APP_TOKEN: YOUR_APP_TOKEN / TABLE_ID: YOUR_TABLE_ID）
- 截止日期为今天且状态<>已完成且状态<>取消的任务（视为今日待办）
- 截止日期为今天且状态=已完成的任务（今日已完成）

【输出格式】
1. 上午复盘（今日已完成 X 项）
2. 今日待办（任务名、状态、截止日期、优先级）
3. 下午建议（优先处理紧急且重要的任务）`,
    message_en: `Good afternoon! Before diving in, suggest clearing emails and pending tasks for 30 min to get organized.

【Workday Check】
Call http://api.jiejiariapi.com/v1/holidays/2026 → check isOffDay(false=workday/adjustment) → continue if yes, otherwise stay silent

【Feishu Bitable Query】（APP_TOKEN: YOUR_APP_TOKEN / TABLE_ID: YOUR_TABLE_ID）
- Completed: deadline = today AND status = Done
- Incomplete: start time <= today AND status <> Done AND status <> Cancelled

【Output Format】
1. Morning review (completed X items today)
2. Today's to-do (name, status, deadline, priority)
3. Afternoon suggestion (prioritize urgent and important tasks)`,
  },
  {
    name: '工作日下班前复盘',
    cron: '30 17 * * 1-5',
    tz: 'Asia/Shanghai',
    description_zh: '每周一至周五 17:30 发送 PMBOK 式进度复盘（完成率/未完成决策/明日预告）',
    description_en: 'Weekdays (Mon-Fri) at 17:30 - sends PMBOK-style progress review (completion rate / pending decisions / tomorrow preview)',
    message_zh: `下班前复盘来了~

【工作日判断】
调用 http://api.jiejiariapi.com/v1/holidays/2026 → 查 isOffDay(false=工作日/调休) → 是则继续，否则静默

【飞书多维表格查询】（APP_TOKEN: YOUR_APP_TOKEN / TABLE_ID: YOUR_TABLE_ID）
- 截止日期为今天且状态<>已完成且状态<>取消的任务（今日未完成）
- 截止日期为今天且状态=已完成的任务（今日已完成）

【PMBOK 五步复盘输出格式】
1. 量化复盘（今日截止共X项，完成X项，完成率X%）
2. 未完成任务逐一决策（推迟/取消/已有计划）
3. 阻碍分析（询问原因，填入存在问题字段）
4. 工作建议（风险应对：规避/转移/缓解/接受；进度纠偏：赶工/快速跟进/调整范围）
5. 明日预告（截止日期为明天且状态<>已完成且状态<>取消的重要任务）`,
    message_en: `Before end-of-day review time!

【Workday Check】
Call http://api.jiejiariapi.com/v1/holidays/2026 → check isOffDay(false=workday/adjustment) → continue if yes, otherwise stay silent

【Feishu Bitable Query】（APP_TOKEN: YOUR_APP_TOKEN / TABLE_ID: YOUR_TABLE_ID）
- Completed: deadline = today AND status = Done
- Incomplete: start time <= today AND status <> Done AND status <> Cancelled

【PMBOK 5-Step Review Output Format】
1. Quantified review (X total, X completed, X% done today)
2. Pending task decisions one by one (defer/cancel/has follow-up plan)
3. Bottleneck analysis (ask for reasons, fill Issues field)
4. Work suggestions (risk response: avoid/transfer/mitigate/accept; schedule recovery: crash/fast-track/scope change)
5. Tomorrow preview (important & urgent tasks with status <> Done AND status <> Cancelled)`,
  },
];

// 以下为剩余代码（不变）
function escapeForCli(str) {
  return str
    .replace(/"/g, '\\"')
    .replace(/'/g, "\\'")
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '')
    .replace(/≤/g, '<=')
    .replace(/≥/g, '>=')
    .replace(/≠/g, '<>')
    .replace(/～/g, '~');
}

function formatMessage(message) {
  const now = new Date();
  const yyyy = now.getFullYear();
  const mm = String(now.getMonth() + 1).padStart(2, '0');
  const dd = String(now.getDate()).padStart(2, '0');
  const dateStr = `${yyyy}-${mm}-${dd}`;
  const weekdayMap = ['日', '一', '二', '三', '四', '五', '六'];
  const weekdayStr = weekdayMap[now.getDay()];
  return message
    .replace(/\{DATE\}/g, dateStr)
    .replace(/\{YEAR\}/g, yyyy)
    .replace(/\{MONTH\}/g, String(now.getMonth() + 1))
    .replace(/\{DAY\}/g, String(now.getDate()))
    .replace(/\{WEEKDAY\}/g, weekdayStr);
}

function buildCronCommand(reminder, lang, userOpenId) {
  const rawMessage = lang === 'zh' ? reminder.message_zh : reminder.message_en;
  const escapedMessage = escapeForCli(formatMessage(rawMessage));
  const description = lang === 'zh' ? reminder.description_zh : reminder.description_en;

  return `openclaw cron add --name "${reminder.name}" --description "${description}" --cron "${reminder.cron}" --tz "${reminder.tz}" --message "${escapedMessage}" --announce --channel feishu --to "user:${userOpenId}" --session isolated --session-key "agent:main:feishu:direct:${userOpenId}" --json`;
}

function executeCron(command, reminder, lang) {
  try {
    const output = execSync(command, {
      encoding: 'utf8',
      timeout: 30000,
      windowsHide: true
    });

    let result;
    try {
      result = JSON.parse(output.trim());
    } catch {
      result = { success: true, output: output.trim() };
    }

    if (result.success !== false && result.id) {
      console.log(lang === 'zh'
        ? `   ✅ ${reminder.name} 创建成功 (ID: ${result.id})`
        : `   ✅ ${reminder.name} created (ID: ${result.id})`);
      return { success: true, id: result.id };
    } else {
      console.log(lang === 'zh'
        ? `   ❌ ${reminder.name} 创建失败: ${result.message || output}`
        : `   ❌ ${reminder.name} failed: ${result.message || output}`);
      return { success: false, error: result.message || output };
    }
  } catch (error) {
    const errMsg = error.stdout || error.stderr || error.message;
    console.log(lang === 'zh'
      ? `   ❌ ${reminder.name} 执行失败: ${errMsg}`
      : `   ❌ ${reminder.name} execution failed: ${errMsg}`);
    return { success: false, error: errMsg };
  }
}

async function main() {
  const lang = getLanguage() || 'zh';

  const args = process.argv.slice(2);
  let userOpenId = null;
  for (const arg of args) {
    if (arg.startsWith('--open-id=')) {
      userOpenId = arg.split('=')[1];
    }
  }

  if (!userOpenId) {
    try {
      const prefPath = path.join(__dirname, '..', 'memory', 'preferences.json');
      if (fs.existsSync(prefPath)) {
        const pref = JSON.parse(fs.readFileSync(prefPath, 'utf8'));
        userOpenId = pref.openId || null;
      }
    } catch (e) {}
  }

  if (!userOpenId) {
    console.log(lang === 'zh'
      ? '\n⚠️  未提供 --open-id 参数，定时提醒将无法发送到正确用户。'
      : '\n⚠️  --open-id not provided, reminders cannot be sent to the correct user.');
    console.log(lang === 'zh'
      ? '    请使用: node scripts/setup_reminders.js --open-id "USER_OPEN_ID"'
      : '    Usage: node scripts/setup_reminders.js --open-id "USER_OPEN_ID"');
    process.exit(1);
  }

  console.log(lang === 'zh'
    ? '\n🔔 开始设置三条定时提醒...'
    : '\n🔔 Setting up 3 scheduled reminders...');

  const results = [];
  for (let i = 0; i < REMINDERS.length; i++) {
    const reminder = REMINDERS[i];
    const command = buildCronCommand(reminder, lang, userOpenId);

    if (lang === 'zh') {
      console.log(`\n【${i + 1}】${reminder.name}`);
      console.log(`   时间: ${reminder.cron} @ ${reminder.tz}`);
    } else {
      console.log(`\n【${i + 1}】${reminder.name}`);
      console.log(`   Schedule: ${reminder.cron} @ ${reminder.tz}`);
    }

    const result = executeCron(command, reminder, lang);
    results.push({ reminder: reminder.name, ...result });
  }

  const successCount = results.filter(r => r.success).length;
  const failCount = results.filter(r => !r.success).length;

  console.log(lang === 'zh'
    ? `\n📊 定时提醒设置完成：成功 ${successCount}/3`
    : `\n📊 Reminder setup complete: ${successCount}/3 succeeded`);

  if (failCount > 0) {
    console.log(lang === 'zh'
      ? `\n⚠️  有 ${failCount} 条提醒创建失败，请检查错误信息。`
      : `\n⚠️  ${failCount} reminder(s) failed - please check error messages above.`);
  } else {
    console.log(lang === 'zh'
      ? '✅ 全部设置成功，每天会在对应时间自动收到工作提醒！'
      : '✅ All reminders set up successfully! You\'ll receive work reminders at scheduled times daily.');
  }

  return {
    success: failCount === 0,
    results
  };
}

main().then(result => {
  process.exit(result.success ? 0 : 1);
}).catch(err => {
  console.error('❌ Unexpected error:', err.message);
  process.exit(1);
});

path: scripts/set_config.js
content: /**
 * 动态更新飞书 API 凭证配置
 * 用法:
 *   node set_config.js --app-id "cli_xxx" --app-secret "xxx" --app-token "xxx" --table-id "xxx"
 *   node set_config.js --app-token "NEW_TOKEN" --table-id "NEW_TABLE_ID"
 *   node set_config.js --list
 * 
 * 说明：
 *   - 不带参数时显示当前配置
 *   - 支持部分更新，只提供想修改的字段即可
 *   - 修改前会显示旧值和新值供确认
 */

const fs = require('fs');
const path = require('path');

// 当前配置（从 config.js 读取）
const CONFIG_PATH = path.join(__dirname, 'config.js');

function readConfig() {
  const content = fs.readFileSync(CONFIG_PATH, 'utf8');
  const match = content.match(/const CONFIG = \{([^}]+)\}/s);
  if (!match) return null;
  
  const config = {};
  const pairs = match[1].split(',');
  pairs.forEach(pair => {
    const [key, value] = pair.split(':').map(s => s.trim());
    if (key && value) {
      const cleanValue = value.replace(/['"]/g, '').trim();
      config[key] = cleanValue;
    }
  });
  return config;
}

function parseArgs() {
  const args = process.argv.slice(2);
  const config = {};
  for (let i = 0; i < args.length; i += 2) {
    const key = args[i].replace('--', '');
    const value = args[i + 1];
    config[key] = value;
  }
  return config;
}

function updateConfig(updates) {
  let content = fs.readFileSync(CONFIG_PATH, 'utf8');
  
  for (const [key, value] of Object.entries(updates)) {
    // 匹配类似 APP_ID: 'xxx' 的模式
    const regex = new RegExp(`(${key}\\s*:\\s*)'[^']*'`, 'g');
    if (regex.test(content)) {
      content = content.replace(regex, `$1'${value}'`);
    } else {
      // 如果没找到，尝试匹配多行配置块
      console.log(`⚠️  ${key} 在配置中未找到，跳过`);
    }
  }
  
  fs.writeFileSync(CONFIG_PATH, content, 'utf8');
  return true;
}

function showCurrentConfig() {
  const config = readConfig();
  if (!config) {
    console.error('❌ 无法读取当前配置');
    return;
  }
  
  console.log('\n📋 当前飞书 API 凭证配置：\n');
  console.log(`   APP_ID:    ${config.APP_ID || '(未设置)'}`);
  console.log(`   APP_SECRET: ${config.APP_SECRET ? '******' : '(未设置)'}`);
  console.log(`   APP_TOKEN: ${config.APP_TOKEN || '(未设置)'}`);
  console.log(`   TABLE_ID:  ${config.TABLE_ID || '(未设置)'}`);
  console.log();
}

async function main() {
  const args = parseArgs();
  
  // --list 显示当前配置
  if (args.list) {
    showCurrentConfig();
    return;
  }
  
  // 不带参数时显示帮助
  if (Object.keys(args).length === 0) {
    console.log('\n📝 set_config.js - 动态更新飞书 API 凭证');
    console.log('\n用法:');
    console.log('   node set_config.js --list                              # 显示当前配置');
    console.log('   node set_config.js --app-token "NEW_TOKEN"             # 更新 APP_TOKEN');
    console.log('   node set_config.js --table-id "NEW_TABLE_ID"           # 更新 TABLE_ID');
    console.log('   node set_config.js --app-id "cli_xxx" --app-secret "xxx"  # 同时更新多个字段');
    console.log('\n示例:');
    console.log('   node set_config.js --app-token "YOUR_APP_TOKEN" --table-id "YOUR_TABLE_ID"');
    showCurrentConfig();
    return;
  }
  
  // 读取当前配置
  const currentConfig = readConfig();
  if (!currentConfig) {
    console.error('❌ 无法读取当前配置，请检查 config.js 是否存在');
    process.exit(1);
  }
  
  // 过滤掉无效参数
  const validKeys = ['app-id', 'app-secret', 'app-token', 'table-id'];
  const updates = {};
  
  for (const [key, value] of Object.entries(args)) {
    if (validKeys.includes(key)) {
      // 转换为 CONFIG 中的键名（APP_ID, APP_SECRET 等）
      const configKey = key.toUpperCase().replace('-', '_');
      updates[configKey] = value;
    }
  }
  
  if (Object.keys(updates).length === 0) {
    console.error('❌ 未提供有效的配置参数');
    console.log('   有效参数: --app-id, --app-secret, --app-token, --table-id');
    process.exit(1);
  }
  
  // 显示变更预览
  console.log('\n📝 配置变更预览：\n');
  for (const [key, newValue] of Object.entries(updates)) {
    const oldValue = currentConfig[key] || '(未设置)';
    if (key === 'APP_SECRET' && oldValue !== '(未设置)') {
      console.log(`   ${key}: ${'******'} → ${newValue ? '******' : '(未设置)'}`);
    } else {
      console.log(`   ${key}: ${oldValue} → ${newValue || '(未设置)'}`);
    }
  }
  console.log();
  
  // 执行更新
  updateConfig(updates);
  
  console.log('✅ 配置文件已更新！\n');
  showCurrentConfig();
}

main();

path: scripts/update_header.js
content: const fs = require('fs');
const content = fs.readFileSync('C:\\Users\\Admin\\.openclaw\\workspace-robotB\\feishu-worklist\\SKILL.md', 'utf8');
const marker = '\uFEFF';
const cleanContent = content.startsWith(marker) ? content.slice(1) : content;
const lines = cleanContent.split('\n');

let startIdx = lines.findIndex(l => l.startsWith('---'));
let endIdx = lines.findIndex((l, i) => i > startIdx && l.startsWith('---'));

const rest = lines.slice(endIdx).join('\n');

const newHeader = `---
name: feishu-worklist
description: |
  飞书多维表格个人工作台账管理系统。帮助用户在 OpenClaw + 飞书环境下搭建个人工作清单管理台账。

  **【强制】触发后必读规则:**
  听到以下任意触发词后，**必须先读取本 SKILL.md 全文并严格按文档流程执行**，不得跳过文档直接处理：
  - 中文: "创建个人工作台账" / "记一下这个工作" + 任务内容
  - English: "create worklist" / "add task" + task content

  **强触发词(直接执行):**
  - 中文: "创建个人工作台账" / "记一下这个工作" + 任务内容
  - English: "create worklist" / "add task" + task content

  **弱触发词(询问确认):**
  - 中文: 用户发送工作相关内容(时间节点、优先级、任务描述等)→ 询问"是否要录入工作台账?"
  - English: User sends work-related content (time, priority, task description) → ask "Would you like to add this to your worklist?"

  **异常诊断触发:**
  - 中文: "台账不工作了" / "数据没更新" / "操作没反应" / "诊断"
  - English: "worklist not working" / "data not updated" / "no response" / "diagnose"
  → 触发 health_check.js 执行5项快速诊断(网络/凭证/表格/字段)

  **排除条件:**
  - 与工作任务无关的内容不录入
  - 用户明确表示不需要记录的内容不录入
  - 非工作相关的闲聊不处理

  **🚨 系统操作红线(强制):**
  - 本 skill 全生命周期内,任何涉及修改用户电脑配置、OpenClaw 配置、系统环境变量的操作,**必须经用户明确确认后方可实施**
  - 未获确认前,此类操作严格禁止,违者视为违反使用规范
  - 可安全执行的操作(读写文件、调用飞书 API、数据处理)不受此限
---`;

fs.writeFileSync('C:\\Users\\Admin\\.openclaw\\workspace-robotB\\feishu-worklist\\SKILL.md', newHeader + '\n' + rest, 'utf8');
console.log('done');

path: scripts/update_task.js
content: /**
 * 更新任务到飞书多维表格
 * 用法:
 *   node update_task.js --record-id "recxxx" --status "已完成" [--lang zh]
 *   node update_task.js --record-id "recxxx" --status "Cancelled" --lang en
 *   node update_task.js --record-id "recxxx" --deadline "2026-04-25" [--lang zh]
 *   node update_task.js --record-id "recxxx" --problem "具体阻碍描述" [--lang zh]
 *   node update_task.js --record-id "recxxx" --notes "备注内容" [--lang zh]
 *   node update_task.js --record-id "recxxx" --start "2026-04-23"
 *   node update_task.js --record-id "recxxx" --duration "3h"
 *   node update_task.js --record-id "recxxx" --stakeholders "张三,李四"
 *   # 可组合使用：
 *   node update_task.js --record-id "recxxx" --status "进行中" --problem "超时了" --lang zh
 */

const { apiPut } = require('./request');
const { CONFIG } = require('./config');
const { getLanguage, getTemplates, format } = require('./i18n');

function parseArgs() {
  const args = process.argv.slice(2);
  const config = {};
  for (let i = 0; i < args.length; i += 2) {
    const key = args[i].replace('--', '');
    const value = args[i + 1];
    config[key] = value;
  }
  return config;
}

// 相对日期转换为具体日期（禁止写入"今天/明天/后天"等模糊词）
function parseRelativeDate(dateStr) {
  if (!dateStr || typeof dateStr !== 'string') return dateStr;
  
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const lower = dateStr.toLowerCase();
  
  if (lower === '今天' || lower === 'today') return formatDateStr(today);
  if (lower === '明天' || lower === 'tomorrow') {
    const d = new Date(today); d.setDate(d.getDate() + 1); return formatDateStr(d);
  }
  if (lower === '后天' || lower === 'day after tomorrow') {
    const d = new Date(today); d.setDate(d.getDate() + 2); return formatDateStr(d);
  }
  
  const amPmMatch = lower.match(/^(今天|明天|后天)(上午|下午|晚上)?$/);
  if (amPmMatch) {
    const dayWord = amPmMatch[1];
    const timePart = amPmMatch[2] || '';
    let targetDay;
    if (dayWord === '今天') targetDay = new Date(today);
    else if (dayWord === '明天') { targetDay = new Date(today); targetDay.setDate(targetDay.getDate() + 1); }
    else if (dayWord === '后天') { targetDay = new Date(today); targetDay.setDate(targetDay.getDate() + 2); }
    if (timePart === '上午') return formatDateStr(targetDay) + ' 09:00';
    if (timePart === '下午') return formatDateStr(targetDay) + ' 14:00';
    if (timePart === '晚上') return formatDateStr(targetDay) + ' 18:00';
    return formatDateStr(targetDay);
  }
  
  const weekMatch = lower.match(/^本周([一二三四五六日天])/);
  if (weekMatch) {
    const dayMap = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7 };
    const targetWeekday = dayMap[weekMatch[1]];
    const currentWeekday = today.getDay() || 7;
    let diff = targetWeekday - currentWeekday;
    if (diff <= 0) diff += 7;
    const targetDay = new Date(today);
    targetDay.setDate(targetDay.getDate() + diff);
    return formatDateStr(targetDay);
  }
  
  const nextWeekMatch = lower.match(/^下周([一二三四五六日天])/);
  if (nextWeekMatch) {
    const dayMap = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7 };
    const targetWeekday = dayMap[nextWeekMatch[1]];
    const currentWeekday = today.getDay() || 7;
    let diff = targetWeekday - currentWeekday + 7;
    const targetDay = new Date(today);
    targetDay.setDate(targetDay.getDate() + diff);
    return formatDateStr(targetDay);
  }
  
  return dateStr;
}

function formatDateStr(date) {
  const y = date.getFullYear();
  const m = String(date.getMonth() + 1).padStart(2, '0');
  const d = String(date.getDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
}

// 中文字段名 → 英文字段名映射
const FIELD_NAME_MAP = {
  '任务名称': 'Task Name',
  '来源分类': 'Source Category',
  '优先级': 'Priority',
  '状态': 'Status',
  '截止日期': 'Deadline',
  '预计时长': 'Estimated Time',
  '开始时间': 'Start Time',
  '工作链接': 'Work Link',
  '工作要求': 'Requirements',
  '干系人': 'Stakeholder',
  '存在问题': 'Issues',
  '备注': 'Notes'
};

// 优先级中英对照
const PRIORITY_MAP = {
  '①紧急且重要': '① Urgent and Important',
  '②重要不紧急': '② Not Urgent but Important',
  '③紧急不重要': '③ Urgent but Not Important',
  '④不紧急不重要': '④ Not Urgent and Not Important',
  '① Urgent and Important': '① Urgent and Important',
  '② Not Urgent but Important': '② Not Urgent but Important',
  '③ Urgent but Not Important': '③ Urgent but Not Important',
  '④ Not Urgent and Not Important': '④ Not Urgent and Not Important'
};

// 状态中英对照
const STATUS_MAP = {
  '待办': 'To Do',
  '进行中': 'In Progress',
  '已完成': 'Done',
  '取消': 'Cancelled',
  'To Do': 'To Do',
  'In Progress': 'In Progress',
  'Done': 'Done',
  'Cancelled': 'Cancelled'
};

// 获取多维表格实际字段名（根据语言）
function getFieldName(zhName, lang) {
  if (lang === 'en') {
    return FIELD_NAME_MAP[zhName] || zhName;
  }
  return zhName;
}

// 转换字段值（优先级/状态）- 中文模式直接返回原值，不再翻译
function translateFieldValue(fieldName, value, lang) {
  return value;
}

async function updateTask(config) {
  if (!config['record-id']) {
    const lang = config.lang || getLanguage();
    const t = getTemplates(lang);
    console.error(lang === 'zh' ? '❌ record-id 不能为空' : '❌ record-id is required');
    console.error(lang === 'zh' ? `   用法: node update_task.js --record-id "recxxx" --status "已完成"` : `   Usage: node update_task.js --record-id "recxxx" --status "Done"`);
    process.exit(1);
  }

  const lang = config.lang || getLanguage();
  const fields = {};

  // 状态字段
  if (config.status) {
    const fieldName = getFieldName('状态', lang);
    const translatedValue = translateFieldValue('状态', config.status, lang);
    fields[fieldName] = translatedValue;
  }

  // 截止日期字段（相对日期转换）
  if (config.deadline) {
    const parsedDeadline = parseRelativeDate(config.deadline);
    const date = new Date(parsedDeadline);
    date.setHours(0, 0, 0, 0);
    const fieldName = getFieldName('截止日期', lang);
    fields[fieldName] = date.getTime();
  }

  // 备注字段
  if (config.notes) {
    const fieldName = getFieldName('备注', lang);
    fields[fieldName] = config.notes;
  }

  // 工作要求字段
  if (config.requirements) {
    const fieldName = getFieldName('工作要求', lang);
    fields[fieldName] = config.requirements;
  }

  // 存在问题字段
  if (config.problem) {
    const fieldName = getFieldName('存在问题', lang);
    fields[fieldName] = config.problem;
  }

  // 干系人字段
  if (config.stakeholders) {
    const fieldName = getFieldName('干系人', lang);
    fields[fieldName] = config.stakeholders;
  }

  // 开始时间字段（相对日期转换）
  if (config.start) {
    const parsedStart = parseRelativeDate(config.start);
    const startDate = new Date(parsedStart);
    startDate.setHours(0, 0, 0, 0);
    const fieldName = getFieldName('开始时间', lang);
    fields[fieldName] = startDate.getTime();
  }

  // 预计时长字段
  if (config.duration) {
    const fieldName = getFieldName('预计时长', lang);
    fields[fieldName] = config.duration;
  }

  const result = await apiPut(
    `/bitable/v1/apps/${CONFIG.APP_TOKEN}/tables/${CONFIG.TABLE_ID}/records/${config['record-id']}`,
    { fields }
  );

  return result;
}

async function main() {
  try {
    const config = parseArgs();
    const lang = config.lang || getLanguage();
    const t = getTemplates(lang);

    console.log(lang === 'zh' ? '🔄 更新任务:' : '🔄 Updating task:', config['record-id']);
    const result = await updateTask(config);

    if (result.code === 0) {
      console.log(t.confirm.updateSuccess);
    } else {
      console.error(format(t.confirm.updateFailed, { msg: result.msg }));
      process.exit(1);
    }
  } catch (error) {
    const lang = getLanguage();
    const t = getTemplates(lang);
    console.error(format(t.confirm.updateFailed, { msg: error.response?.data?.msg || error.message }));
    process.exit(1);
  }
}

main();

path: scripts/utils.js
content: /**
 * 飞书 API 工具函数
 * 
 * 内部封装：
 * - Token 缓存 + 提前刷新
 * - 统一超时控制
 * - 401 自动重试
 * 
 * 外部脚本应使用 request.js 的 apiGet / apiPost / apiPut
 */

const { getAccessToken } = require('./request');

module.exports = { getAccessToken };

path: examples/README.md
content: # examples/ - 示例文件

本目录用于存放正例和反例输出样本，帮助用户理解正确和错误的行为方式。

## 当前示例

暂无预设示例，建议用户在实际使用中积累自己的正例和反例。

---

**正例积累方向：**
- 任务录入成功的对话示例（包含完整的字段收集和确认）
- 日期转换正确的对话示例（展示三步验证过程）
- 定时提醒输出的正确格式
- 下班前复盘5步闭环的完整示例

**反例积累方向：**
- 日期转换跳步的错误示例（本周五 → 04-25 而不是 04-24）
- 字段放置位置错误的示例（工作要求放到了备注列）
- 状态变更未立即执行的错误示例

---

*如有需要，可上传示例对话或截图到本目录。*

path: examples/test-prompts.md
content: # feishu-worklist 测试Prompt

## 应触发（✅）

**Prompt 1：**
> "创建个人工作台账"

**预期行为：** 进入台账创建流程，依次检查飞书 API 权限、引导确认表格字段、要求用户配置凭证。

---

**Prompt 2：**
> "记一下这个工作，4月市场侧宽带团单施工协调跟进，截止日期4月27日，优先级紧急且重要，干系人是雨凡和洪兴"

**预期行为：** 
1. 识别四个必填字段齐全，立即写入多维表格
2. 不等待可选字段（预计时长等）咨询完毕
3. 录入后告知成功（含 record_id）

---

## 不应触发（❌）

**Prompt 3：**
> "今天天气真好啊，周末想去爬山"

**预期行为：** 不触发任何工作台账相关流程，视为闲聊。

---

**Prompt 4：**
> "帮我算一下这个月的报销"

**预期行为：** 不是工作任务，不录入工作台账，告知用户这不是工作清单管理范围。

---

## 边界情况（⚠️）

**Prompt 5：**
> "这个工作截止日期是下周三，记得提醒我"

**预期行为：** 
1. 正确识别"下周三"为相对日期
2. 三步验证：今天4月23日（周四）→ 下周三 = 4月28日（周一+4天）
3. 在回复中显式写出"2026-04-28（下周三）"
4. 能正确录入到多维表格

Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)