diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..ef455d2 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "钉钉文档": { + "type": "streamable-http", + "url": "https://mcp-gw.dingtalk.com/server/fcf2405ec27cd4428220e0515ff8e04ee52ba7e11578aabbd1ef57651ad9c4d4?key=70fb66762bdf5b10a304a3d1a270aebb" + } + } +} diff --git a/TOOLS.md b/TOOLS.md index 917e2fa..e5034d2 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -38,3 +38,29 @@ Skills are shared. Your setup is yours. Keeping them apart means you can update --- Add whatever helps you do your job. This is your cheat sheet. + +## 钉钉应用配置(企业内部应用) + +用于操作日历、查看日志、文档等企业钉钉功能。 + +```markdown +AgentId: 4404185308 +Client ID (AppKey): dingklemniq8uqk5qbgx +Client Secret (AppSecret): _8EHgyhvHRHRMx6fZbh9LNpQoxyYl3At0b-fXXlQiahwupbt9oY5P6Grj8IM9Dx8 +``` + +## 钉钉 API 注意事项 + +### ⚠️ 时间参数单位:毫秒(ms) +钉钉绝大名接口的时间参数(如 `start_time`、`end_time`、`created_at` 等)都是**毫秒**,不是秒! +- ❌ 错误:`1743206400`(秒) +- ✅ 正确:`1743206400000`(毫秒) + +### ⚠️ 字段命名:全小写 `userid` +很多接口参数名是 `userid`(全小写),不是 `userId`(驼峰)。具体以接口文档为准。 + +## 日志查询 + +参考文档:https://open.dingtalk.com/document/development/query-logs-sent-by-an-employee + +详细调用方式见 skill:`skills/dingtalk-log/SKILL.md` diff --git a/memory/luoguocai_weekly_reports.md b/memory/luoguocai_weekly_reports.md new file mode 100644 index 0000000..bf22fdc --- /dev/null +++ b/memory/luoguocai_weekly_reports.md @@ -0,0 +1,193 @@ +# 罗国财 工作周报记录 + +## 2026-03-24 经理人周报 + +**本周完成工作:** +1. 标杆名单项目需求推进(4月份规划内容) +2. 大模型落地项目(导购及智能装修功能的思考和方案探索) +3. 3月份迭代事项(推进AI需求的目标落地) +4. 客如云项目(关注高价值需求的推进) +5. 前端员工面谈 + +**本周工作总结:** +关于AI + 多维表格的思考(在AI创新小组的思考,分享给大家) + +1)多维表格背景 +①、低代码平台演变:Paas平台 -> aPaas平台 ->低代码平台 ->无代码平台 -> 多维表格 +②、aPaas与低代码平台区别:aPaas平台和低代码平台就有一些重合,网上也有人说他们是一个东西两个名称而已,有人说他们完全不是一回事,并列了一些不止所谓的区别,我认为就是搞概念而已。在我看来,低代码平台应该是aPaas平台的一种,而且大部分时候他们是可以看作是一个事物,若真有不一样,我认为是在代码介入的程度,就是有多低代码。 +③、无代码平台和多维表格区别。它们同样类似上面的aPaas和低代码平台这样,我认为多维表格是无代码平台的一种,也可以现在所谓的无代码平台基本就是指多维表格这种特定的应用。 +④、产品演变方向。从演变过程来看,就是从复杂庞大专业到简单大众化,尤其在近这一两年来,多维表格这种无代码平台完胜低代码平台。从钉钉在主菜单的最中间位置上去放多维表格的入口就可见一斑。 +⑤、从代码平台演变看PMF。从功能上来看,低代码平台可以做无代码实现的任何事情,但是无代码不能做部分低代码平台的事情。例如钉钉前几年大力发展的宜搭,可以做飞书多维表格实现的任何事情,但是事实就是被打很惨。大家可能说不是同一个赛道,但是问题是:在10个用户里面,或许有8个客户的问题是可以通过多维表格就可以解决,只有2个才是用宜搭去解。那结果就很显然易见:那8个客户不会花那么大的学习曲线去学习复杂的宜搭。另外一个就是多维表格的展现形式就是一张我们最常见的Excel表格,这也让大部分人可以直接上手使用,可以从浅到深去使用,产品受众并不是只有企业专业用户。相反,类似宜搭的产品,复杂问题用它去解,很麻烦,成本也不少,他们的受众IT相关人员也不爱用,限制太多。所以最终造就了一个尴尬的局面:简单的问题多维表格解决就够了,复杂的问题貌似宜搭也解决不了。 + +2)多维表格 + AI +①、国外厂商:Airtable,创立于2013年,在2021年12月完成7.35亿美元F轮融资,迄今为止,Airtable共计获得13.6亿美元融资,估值117亿美元。(飞书官网谈多维表格竞品就谈它) +②、国内厂商:飞书多维表格、钉钉多维表格、维格,这三家不多介绍,其中维格是创业公司,上次见到他们其中一个创始人,说是他们先做的多维表格,后面飞书 + +**下周工作计划:** +1. 标杆名单项目需求推进(4月份规划内容落地执行) +2. 大模型落地项目(解决经营分析助手的回答准确率问题) +3. 4月份迭代目标确认(关注AI的4月目标) +4. 客如云项目(已明确的需求交付周期确定) +5. 前端员工面谈(下周关闭) + +--- + +## 2026-03-17 经理人周报 + +**本周完成工作:** +1. 标杆名单项目需求推进(4月份规划内容) +2. 大模型落地项目(导购及智能装修功能的思考和方案探索) +3. 3月份迭代事项(推进AI需求的目标落地) +4. 客如云项目(关注高价值需求的推进) +5. 前端员工面谈 + +**本周工作总结:** +1、精细化管理 +最近一阵都有接触客如云相关的项目,在和对方去沟通客户分层和商机等相关事项,我会发现对方在在这两点上的管理有一些地方可以值得我们去学习。 + +1)客户分层定位。客如云把餐饮连锁行业分成15种客群,每个都会有一个客户画像编号,对应有这个客户群的特点精准描述。在我们一开始看来是觉得比较不可思议:一个的餐饮连锁可以细分出那么多客群来。在我们最初的行业划分里面,我们只是把整个行业划分成13个行业,像餐饮连锁,我们根本没有关注里面到底怎么去区分。也就是在上一年,我们才把二级行业也做了一个细分,部分行业也分得更加细。如果从产品规划的角度去看,行业细分最终可以让我们在产品上做对应的隔离,针对不同的细分行业,可以有更加专业实用的功能输出。不过这一个主要还是取决于我们是否可以有对应的资源去隔离。但是至少我们可以有这个选项。例如当下我们在自己擅长的行业就可以去做对应的事情,而不是自己的甜蜜行业,那应该就是按照之前的标准,哪怕是区分了更细的二级行业,但是我们还是暂时不去处理。而在销售管理的这个优势就更加明显了,针对不一样的细分客户分层,我们可以采用不一样的销售术语,提供对应的解决方案,给客户展示对应相同类型的客户案例等等。 + +2)商机管理。我们每一次和客如云进行周会,客如云都会给我们同步最近时间的商机情况,我们可以很清晰看到在,这段时间一共有多少商机,有效的多少,无效的多少。有效里面已经完成审批的有多少,审批通过的有多少是符合铱云供应链 + 客如云解决方案的,其中符合画像的有多少,不符合的有多少。对于那些不符合画像的客户里面,我们也能清楚看到分类原因是:产品功能原因 这个一个分类。这样我们很清楚,这些客户因为那些产品的功能而没有买单。回到我们产品规划上,我们就可以有更加清晰的导向,到底去规划哪些需求哪些内容。我们现在其实是比较缺失这样的管理。我们大多时候是听到有客户有抱怨某个功能没有导致客户没有成交,但是总是只是听到这样的声音和甚至有时候是情绪,也不知道这个声音到底是怎样的一个比例,所以我们根本无法看到商客户市场的一个全貌。如果这些信息我们产研都不够清晰,那后续规划如何去做到更加贴合客户呢?我希望后续产研这边可以和朱文建把这个事情梳理一下,是否有持续的商品精细化数据分析。 + +PS:下面几个图是客如云内的,关键数据已经打了马赛克 +[图片] +[图片] +[图片] + +2、常牧客户(客户案例) + +**下周工作计划:** +1. 标杆名单项目需求推进(4月份规划内容) +2. 大模型落地项目(导购及智能装修功能的思考和方案探索) +3. 3月份迭代事项(推进AI需求的目标落地) +4. 客如云项目(关注高价值需求的推进) +5. 前端员工面谈 + +--- + +## 2026-03-10 经理人周报 + +**本周完成工作:** +1. 标杆名单调研报告阅读及分析(明确第一批12家) +2. 大模型落地项目(关闭内审) +3. 3月份迭代事项(关注AI需求的目标落地) +4. 前端员工面谈(延后持续) + +**本周工作总结:** +1、流失客户复盘 +本周收到刘程通知,告知我们一个知名的冻品客户鸿海将要放弃易订货去采用一个叫金灯塔的软件。我当时第一反应是:金灯塔?是哪家公司的产品。然后上网收集了一些信息,结果什么都没有查到,只看到一个比较简陋的软件登录页面。后面刘程也拉着相关同学做了一个复盘会议。复盘会议里面我们复盘到有两个我们的功能点:AI营销和库存管理相关。 + +这两个功能自己有一些看法。 + +AI营销:我个人会感觉竞品的这个功能是偏营销噱头,目前我们观察到市面上的SaaS产品,也没有一家公司在这个点上有一个非常好的应用。以当下AI大模型的能力去构建智能营销功能有很好的落地还需要一段路要走,需要在Agent工程化上面做很多投入才可以。但是换一句话来说,是不是产品的AI营销功能是可以先有,至于真正的业务价值还是其次? + +库存管理相关:这里面最主要的应该是库位,这是专业软件WMS的范畴,我们之前拜访包老弟这个客户也是提了这个问题,他们希望我们可以提供WMS相关的功能。对于我们一些规模比较大客户,会有这样的需求。但是WMS本来就是一个专业软件,以我们的资源情况,很难短时间去做到这样的能力,而且WMS在市面上价格是比较贵的。对于金灯塔做到那种程度,我是比较好奇的。未来我们可能会有简单的库位管理,但是肯定也会是简单的库位管理,而不是完整的WMS。另外像指掌这种竞品,也号称自己是有WMS模块,但是我们粗略看了一下,也是一个很简单的实现,很难真正实现大型企业的库位管理。 + +抛开一些具象的功能需求,我想谈一些客户流失的看法: + +1)客户流失大多数都是各种因素掺杂在一起导致的结果,应该很少应该1到2个很具体的功能点而放弃。如果有,若真的有,那应该有机制在很早的阶段同步给到产研,产研开始一些介入,然后给出产研的结论:要么去迭代这些功能,要么基于ROI不做就向公司抛出风险。我尝试在我们产研需求池和运营平台里面找海鸿的需求,只找到零星的一两个需求,而且还不是上面提及的内容。重点客户很在意某些功能,在意到可能要去更换系统,而我们竟然没有相关的需求录入? + +2)产研在关键客户流失后,应该有对应的信息收集机制,而不是单方面的获取二手信息。当然不是去说二手信息不准确,而是不一样的位置的人对信息的理解和处理程度也会有不一样。所以这需要我们在后续建立这样的机制,当公司关键客户(可以以服务的L1和L2作为标准)流失,产研需要去做调研分析。 + +3) + +**下周工作计划:** +1. 标杆名单调研报告阅读及分析(关注需求落地) +2. 大模型落地项目(智能营销及客户分析需求外审) +3. 3月份迭代事项(推进AI需求的目标落地) +4. 前端员工面谈 + +--- + +## 2026-03-03 经理人周报 + +**本周完成工作:** +1. 标杆名单调研报告阅读及分析(持续) +2. 大模型落地项目(已完成智能营销及智能客户分析的方案输出) +3. 3月份迭代事项(关注AI需求的目标落地) +4. 经理人面谈(关闭) + +**本周工作总结:** +1、智能营销及智能客户分析方案 +上周我花了不少时间在输出智能营销及智能客户分析两个功能的方案。对于这两个AI大模型的功能,我自己是想有更多的参与,所以我花了一些时间去写了这两个方案。两个方案结构都基本一致,就是对于AI agent开发的一些关键要点做了说明,我分别按照以下自己要点去做输出: + +1)功能说明 +2)关键时序图(大模型、AI Agent、易订货产品三者的关键交互过程) +3)Prompt说明(每一次交互过程用到的Prompt如何编写?) +4)关键数据范围(喂给大模型的的数据包括有哪些?) +5)本地知识说明(提供我们铱云在这个领域的一些业务知识) +6)工具列表(我们可以提供给大模型的API列表) + +我上次年会就分享过,我们所谓的AI Agent = 大模型 + 记忆 + 技能规划 + 工具使用。而我上次说的几点其实就是围绕这四个内容去做说明。例如说我们本地知识说明 + 关键数据范围其实就是上面提及的记忆,而技能规划对应的就是Prompt说明。 + +这次产研把AI相关功能真正作产品功能去落地,很多事情都是第一次接触。所以我也特意去写了这样一个方案说明,这种方案说明有AI技术侧的内容说明,也有业务侧的一些思考。而最后产品经理可以根据这个方案内容说明去进行客户场景调研,输出最终的需求原型。我们之所以先输出一个这样的AI技术方案主要的考虑是AI agent的一些技术方案及限制以及如何最大限度发挥大模型能力这些信息产品经理目前还是不够清晰。最后我们在这个输出原型的过程,整个公司AI创新小组的同学也不断去介入,实时去调整需求原型,已达到我们认知的最匹配客户场景的状态。上述这个过程应该在未来一段时间都是我们去迭代产品AI功能的大致流程。 + +另外我想和大家分享几个点。我们讨论过后,也发现在AI时代去做AI Agent的产品,产品经理关注的点跟过去有都不少变化。 + +1)更强的数据因果分析能力。我们作为一个有大量客户商品及交易数据的公司,很多时候想要做到功能的大致逻辑就是大模型去分析一些数据然后得到一些结论,最终根据结论去执行一些事情。这里我们就要去思考我们的结论是那方面的结论,我们就要考虑去投喂那些业务数据给大模型,这里就有很强的数据因果关系分析。例如说我要做智能营销,那我可能要把客户的登录数据也要给客户,这里面存在登录频次及登录时间这些数据可以让我做智能优惠券可以更精准投放给对应的客户 + +**下周工作计划:** +1. 标杆名单调研报告阅读及分析(明确初步标杆名单) +2. 大模型落地项目(智能营销及客户分析需求原型的内审) +3. 3月份迭代事项(关注AI需求的目标落地) +4. 前端员工面谈 + +--- + +## 2026-02-24 经理人周报 + +**本周完成工作:** +1. 标杆名单第一阶段调研关闭(关注报告数据整理) +2. 大模型落地项目(推进Q1目标) +3. 年会报告内部细化分享 +4. 经理人面谈 + +**本周工作总结:** +1、客户拜访 +本周二去拜访深圳一家冻品客户:尚为食品,也是我们打造冻品标杆行动里面的列名客户。客户是冻品食材行业,日单量大概在150-200左右,GMV大概在3700W左右,在线支付流水1300W。客户主要的业务是冻品这一块,IT系统用了金蝶软件做财务,除财务以外的内容都是用易订货1.0旗舰版,当前已经付费升级了,但是还没有在2.0上面跑业务。客户还有发展另外一块新业务:鲜品,现在这一块业务使用管家婆在处理,但客户想统一整合在2.0上。客户还有发展另外一块新业务:鲜品,现在这一块业务使用管家婆在处理,但客户想统一整合在2.0上。客户和我们上海的客户上奉食品有一定的投资合作关系,所以很多使用系统的方式都参考了上奉。分享两个点: + +1)在线支付。从客户的支付流水来看,我们也看到客户只有大概40%左右的GMV是走我们的在线支付,其他都是走线下的模式。我们也在这个点上问询了客户。客户没有全部使用在线支付,并不是因为手续费或其他一些因素,而是因为下游小B在支付的时候不方便。客户的下游基本都是走月结或许半月结,所以他们支付都是希望可以一次过支付一批订单,而不是一张张订单支付。每次这种按每一个订单进行支付,还经常出现漏了订单没有支付的情况。而我们账单支付功能正正是可以解决这个问题,我们同步给客户之后,客户表示这个功能是他们想要的,后续会引导小B使用这个功能进行支付。(客户的下游都是小商家,所以他们月结或许半月结的账单也大概在5000左右,并不会触发微信支付的一些限额控制)。 + +我们在支付这个事情,是不是也存在很多类似尚为这种类型的客户?我年会里面提到支付里面我们需要对已经使用了在线支付但是在线支付占比不高的客户进行重点关注。在未来,我觉得应该列名去跑这里面GMV比较大客户,去做深度的支付挖掘。 + +2)AI。我在交流中提及到AI这个点,并且向他们老板李总展示了我们的AI的原型功能。李总是一个心态很开放的人,很认同AI大模型在未来会给各行各业赋能这个点。所以他很早就自己使用了ChatGPT,用它来解决自己的一些问题。当我去展示一些我们的原型功能的时候,他是非常感兴趣。并且在我去讲述未来我们将来迭代的一些类似AI营销的功能时候,他表示很认同,认为这些AI营销的功能是他们想要的功能。不过出乎我自己意料之外,在展示这种通过自然语言去查询报表能力的时候,李总会觉得可能他会更习惯自己通过传统方式去查询。而在展示知识问答助手的时候,他表示这种当然可以有,但是他更希望是赋能给到他们的下游,例如下 + +**下周工作计划:** +1. 标杆名单调研报告阅读及分析 +2. 大模型落地项目(完成智能营销功能方案输出) +3. 3月份迭代事项(关注AI需求的目标落地) +4. 经理人面谈(持续) + +--- + +## 2026-02-17 经理人周报 + +**本周完成工作:** +1. 标杆名单调研拜访(关注进度) +2. 大模型落地项目(推进Q1目标) +3. 25年年会报告 + +**本周工作总结:** +1、AI具象化功能点 +[表格] +经过我们内部的一些讨论以及对外部场景的一些洞察,我们基本会落在两个大场景:营销及销售管理上去进行具象功能挖掘。这两个场景的功能,我们已经有大致方案和功能框架,但是一些细节的问题还需要讨论以及和需要与客户做一些相关交流,这样我们确保我们的功能是去解决客户在意的场景问题。至于两个两个功能知识助手和业务数据助手,我们会在这两个月持续迭代优化,在三月末迭代内侧版本发布上线。 + +2、年会报告后续事项 +周末到公司跟区域同学做一个关于年会报告的重点内容分享,主要围绕两个事情: + +1)产品场景化设计。年会报告我提及我们产研在过去拜访的客户使用我们产品的去解决业务场景覆盖非常低的问题。若场景解决低,那意味后续的续费、增购、升级都无法顺利去进行,更不要去谈及圈层经营这一类的事情。所以这也是今年产研是非常重要的事情。首先今年我们加强新功能调研及标杆客户调研这个事情,把客户的业务场景都摸一遍,让我们自身清晰产品的解决场景的问题都在哪些点上,然后根据我们核心 3 +1 策略去逐个解决问题。其次,我们今年也会在内部组织结构上匹配我们产品场景化。在Q1我们产品会重新设计迭代结构,去做到团队和场景挂钩,最终团队考核与客户导向的场景直接相关。 + +2)AI大模型产品创新。年会报告结束后,有不少一线的同学对AI的功能明显非常感兴趣,也有同学已经问了我账号去体验。后续产研会重点投入去加速一些功能的迭代。以此同时,我们也不能去忽略AI产品功能的客户验证,这些功能同样需要去找客户做调研,无论是已经成型的功能,还是将要迭代的功能。 + +这两个事项,在本周我还有拉一个产研全员的会议去做一个加持说明,尤其在AI这个事情上,我需要动员到全产研的同学有更高的参与度以及阐述一些AI创新组织及机制说明公司对这个事情的重视程度。 + +**下周工作计划:** +1. 标杆名单第一阶段调研关闭(关注报告数据整理) +2. 大模型落地项目(推进Q1目标) +3. 年会报告内部细化分享 +4. 经理人面谈 + +--- + +*记录时间:2026-03-29* +*数据来源:钉钉工作日志 API* diff --git a/skills/dingtalk-document/SKILL.md b/skills/dingtalk-document/SKILL.md new file mode 100644 index 0000000..fc780e6 --- /dev/null +++ b/skills/dingtalk-document/SKILL.md @@ -0,0 +1,88 @@ +--- +name: dingtalk-document +description: 钉钉知识库和文档管理操作。当用户提到"钉钉文档"、"知识库"、"新建文档"、"查看文档目录"、"读取文档内容"、"写入文档"、"更新文档"、"文档成员"、"dingtalk doc"、"knowledge base"时使用此技能。支持:创建知识库、查询知识库列表、新建文档/文件夹、读取/写入文档正文内容、管理成员权限等全部文档类操作。 +--- + +# 钉钉文档技能 + +负责钉钉知识库和文档的所有操作。本文件为**策略指南**,仅包含决策逻辑和工作流程。完整 API 请求格式见文末「references/api.md 查阅索引」。 + +> `dt_helper.sh` 位于本 `SKILL.md` 同级目录的 `scripts/dt_helper.sh`。 + +## 核心概念 +- **知识库(Workspace)**:文档容器,有 `workspaceId` 和 `rootNodeId` +- **节点(Node)**:文件或文件夹,`type` 为 `FILE` 或 `FOLDER` +- **文档标识(用于 `/v1.0/doc/suites/documents/{id}`)**:可用 `docKey` 或 `dentryUuid` + - 创建文档响应会返回:`docKey`、`dentryUuid`、`nodeId` + - 其中 `docKey` / `dentryUuid` 可用于读写正文;`nodeId` 用于删除和文档管理类接口 + - `wiki/nodes` 返回的 `nodeId` 实际上是 `dentryUuid`,可直接用于正文读写 +- **operatorId**:所有接口必须的 unionId 参数,通过 `bash scripts/dt_helper.sh --to-unionid` 自动转换 + +## 工作流程(每次执行前) +1. **先识别本次任务类型** → 例如:列知识库、读文档、写文档、创建文档、成员管理 +2. **按本次任务校验所需配置** → 通过 `bash scripts/dt_helper.sh --get KEY` 读取;仅校验本任务必须项 +3. **仅收集缺失配置** → 若缺少某项,**一次性询问用户**所有缺失值,用 `bash scripts/dt_helper.sh --set KEY=VALUE` 写入 +4. **获取 Token / operatorId** → 直接调用 `bash scripts/dt_helper.sh`,token 获取与缓存细节无需关心 +5. **执行操作** → 凡是包含变量替换、管道或多行逻辑的命令,写入 `/tmp/.sh` 再 `bash /tmp/.sh` 执行。不要把多行命令直接粘到终端里(终端工具会截断),也不要用 `<<'EOF'` 语法(heredoc 在工具中同样会被截断导致变量丢失) + +### 按任务校验配置(必须先做) +- **所有任务通用必需**:`DINGTALK_APP_KEY`、`DINGTALK_APP_SECRET`、`DINGTALK_MY_USER_ID` +- **涉及任何文档/知识库 API 调用**:必须有 `DINGTALK_MY_OPERATOR_ID`(若缺失,先用 `bash scripts/dt_helper.sh --to-unionid` 自动转换并写回) +- **创建/读取/写入/删除/成员管理**:除上述通用项外,无额外固定配置键;`workspaceId`/`nodeId`/`docKey` 属于任务参数,运行时从用户输入或 API 响应中获取 + +> 规则:未通过“本次任务配置校验”前,不得进入 API 调用步骤。 + +> 凭证禁止在输出中完整打印,确认时仅显示前 4 位 + `****` + +### 所需配置 +| 配置键 | 必填 | 说明 | 如何获取 | +|---|---|---|---| +| `DINGTALK_APP_KEY` | ✅ | 应用 AppKey | 钉钉开放平台 → 应用管理 → 凭证信息 | +| `DINGTALK_APP_SECRET` | ✅ | 应用 AppSecret | 同上 | +| `DINGTALK_MY_USER_ID` | ✅ | 当前用户的企业员工 ID(userId) | 管理后台 → 通讯录 → 成员管理 → 点击姓名查看 | +| `DINGTALK_MY_OPERATOR_ID` | ✅ | 当前用户的 unionId(operatorId) | 首次由 `bash scripts/dt_helper.sh --to-unionid` 自动转换并写入 | + +### 身份标识说明 +| 标识 | 说明 | +|---|---| +| `userId`(= `staffId`) | 企业内部员工 ID,可通过管理后台 -> 通讯录 -> 成员管理 -> 点击姓名查看 | +| `unionId` | 跨企业/跨应用唯一标识,可通过 `bash scripts/dt_helper.sh --to-unionid ` 获取 | + +### 执行脚本模板 +```bash +#!/bin/bash +set -e +HELPER="./scripts/dt_helper.sh" +NEW_TOKEN=$(bash "$HELPER" --token) +OPERATOR_ID=$(bash "$HELPER" --get DINGTALK_MY_OPERATOR_ID) + +# 在此追加具体 API 调用,例如查询知识库列表: +WORKSPACES=$(curl -s -X GET "https://api.dingtalk.com/v2.0/wiki/workspaces?operatorId=${OPERATOR_ID}&maxResults=20" \ + -H "x-acs-dingtalk-access-token: $NEW_TOKEN") +echo "知识库列表: $WORKSPACES" +``` + +> **Token 失效处理**:dt_helper 仅按时间缓存,无法感知 token 被提前吊销。若 API 返回 401(token 无效/过期),用 `--nocache` 跳过缓存强制重新获取: +> ```bash +> NEW_TOKEN=$(bash "$HELPER" --token --nocache) +> ``` + +## references/api.md 查阅索引 +确定好要做什么之后,用以下命令从 `references/api.md` 中提取对应章节的完整 API 细节(请求格式、参数说明、返回值示例): +```bash +grep -A 30 "^## 1. 查询知识库列表" references/api.md +grep -A 10 "^## 2. 查询知识库信息" references/api.md +grep -A 35 "^## 3. 查询节点列表" references/api.md +grep -A 10 "^## 4. 查询单个节点" references/api.md +grep -A 15 "^## 5. 通过 URL 查询节点" references/api.md +grep -A 28 "^## 6. 创建文档" references/api.md +grep -A 10 "^## 7. 删除文档" references/api.md +grep -A 30 "^## 8. 读取文档内容" references/api.md +grep -A 15 "^## 9. 覆盖写入文档内容" references/api.md +grep -A 12 "^## 10. 追加文本到段落" references/api.md +grep -A 18 "^## 11. 添加文档成员" references/api.md +grep -A 12 "^## 12. 更新文档成员权限" references/api.md +grep -A 10 "^## 13. 移除文档成员" references/api.md +grep -A 10 "^## 错误码" references/api.md +grep -A 10 "^## 所需应用权限" references/api.md +``` diff --git a/skills/dingtalk-document/references/api.md b/skills/dingtalk-document/references/api.md new file mode 100644 index 0000000..f5338da --- /dev/null +++ b/skills/dingtalk-document/references/api.md @@ -0,0 +1,341 @@ +# dingtalk-document API 参考 + +> 所有接口均已验证可用。 +> `NEW_TOKEN` = 新版 token(`api.dingtalk.com` 用),获取方式 `bash scripts/dt_helper.sh --token` +> `OPERATOR_ID` = 用户 unionId,获取方式 `bash scripts/dt_helper.sh --get DINGTALK_MY_OPERATOR_ID` +> ⚠️ **重要**:所有接口均需传 `operatorId`(unionId),缺少则返回 `MissingoperatorId` 错误。 + +--- + +## 1. 查询知识库列表 + +``` +GET https://api.dingtalk.com/v2.0/wiki/workspaces?operatorId={OPERATOR_ID}&maxResults=20&nextToken= +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `operatorId` | string | ✅ | 用户 unionId | +| `maxResults` | int | ❌ | 每页数量,默认 20 | +| `nextToken` | string | ❌ | 分页令牌,首次为空 | + +响应: +```json +{ + "workspaces": [ + { + "workspaceId": "QXvd5SLN2AxOQz0Z", + "name": "团队知识库", + "description": "...", + "rootNodeId": "P0MALyR8kl3qpB7qTkM1xn3mW3bzYmDO", + "type": "TEAM", + "url": "https://alidocs.dingtalk.com/i/spaces/.../overview", + "createTime": "2024-01-01T00:00Z", + "modifiedTime": "2024-06-01T00:00Z" + } + ], + "nextToken": "..." +} +``` + +> 翻页:`nextToken` 非空时传入下次请求继续获取。 + +--- + +## 2. 查询知识库信息 + +``` +GET https://api.dingtalk.com/v2.0/wiki/workspaces/{workspaceId}?operatorId={OPERATOR_ID} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +响应:单个 workspace 对象(结构同列表项) + +--- + +## 3. 查询节点列表 + +``` +GET https://api.dingtalk.com/v2.0/wiki/nodes?parentNodeId={nodeId}&operatorId={OPERATOR_ID}&maxResults=50&nextToken= +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `parentNodeId` | string | ✅ | 父节点 ID,传知识库的 `rootNodeId` 可列出顶层内容 | +| `operatorId` | string | ✅ | 用户 unionId | +| `maxResults` | int | ❌ | 每页数量,默认 20 | +| `nextToken` | string | ❌ | 分页令牌 | + +响应: +```json +{ + "nodes": [ + { + "nodeId": "LeBq413JAw31yaz1fB0BBdLGWDOnGvpb", + "name": "使用文档.adoc", + "type": "FILE", + "category": "ALIDOC", + "extension": "adoc", + "workspaceId": "QXvd5SnBnzmZdZ0Z", + "url": "https://alidocs.dingtalk.com/i/nodes/...", + "createTime": "2026-03-04T16:58Z", + "modifiedTime": "2026-03-04T17:51Z" + } + ], + "nextToken": "..." +} +``` + +> `type`:`FILE`(文档/文件)| `FOLDER`(文件夹) + +--- + +## 4. 查询单个节点 + +``` +GET https://api.dingtalk.com/v2.0/wiki/nodes/{nodeId}?operatorId={OPERATOR_ID} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +响应:`{ "node": { nodeId, name, type, category, workspaceId, url, ... } }` + +--- + +## 5. 通过 URL 查询节点 + +``` +POST https://api.dingtalk.com/v2.0/wiki/nodes/queryByUrl?operatorId={OPERATOR_ID} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +请求体: +```json +{ + "url": "https://alidocs.dingtalk.com/i/nodes/", + "operatorId": "{OPERATOR_ID}" +} +``` + +响应:与 GET 单个节点相同的 node 结构。 + +--- + +## 6. 创建文档 + +``` +POST https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +请求体: +```json +{ + "operatorId": "{OPERATOR_ID}", + "docType": "DOC", + "name": "文档标题" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `operatorId` | string | ✅ | 用户 unionId | +| `docType` | string | ✅ | 固定填 `"DOC"`(ALIDOC 富文本格式) | +| `name` | string | ✅ | 文档标题 | + +响应: +```json +{ + "dentryUuid": "aaa", + "nodeId": "xxx", + "docKey": "yyy", + "workspaceId": "zzz", + "url": "https://..." +} +``` + +> **重要**: +> - `docKey` / `dentryUuid`:用于 `/v1.0/doc/suites/documents/{id}` 内容读写 +> - `nodeId`:用于 `/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}` 删除/管理 + +--- + +## 7. 删除文档 + +``` +DELETE https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}?operatorId={OPERATOR_ID} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +> `workspaceId` 和 `nodeId` 均使用创建文档响应中的值。成功返回 `200 {}`。 + +--- + +## 8. 读取文档内容(Block 结构) + +``` +GET https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/blocks?operatorId={OPERATOR_ID} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +> **docKey 的来源**: +> - 通过 wiki nodes 接口查到的 `nodeId` 本质是 `dentryUuid`,可直接用于正文读写 +> - 通过创建文档接口新建的文档:可使用响应中的 `docKey` 或 `dentryUuid` +> - 创建响应中的 `nodeId`(通常出现在 `/docs/` 链接中)不能直接用于正文读写 + +响应: +```json +{ + "result": { + "data": [ + { "blockType": "heading", "heading": { "level": "heading-2", "text": "快速开始" }, "index": 0, "id": "xxx" }, + { "blockType": "paragraph", "paragraph": { "text": "正文内容..." }, "index": 1, "id": "yyy" }, + { "blockType": "table", "table": { "colSize": 2, "rowSize": 10 }, "index": 2, "id": "zzz" }, + { "blockType": "unknown", "index": 3, "id": "aaa", "unknown": {} } + ] + }, + "success": true +} +``` + +`blockType` 枚举:`heading`、`paragraph`、`unorderedList`、`orderedList`、`table`、`blockquote`、`unknown`(代码块/图片等) + +--- + +## 9. 覆盖写入文档内容 + +``` +POST https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/overwriteContent?operatorId={OPERATOR_ID} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +> ⚠️ `operatorId` 必须**同时**作为 query param 和请求体字段传入,缺一会报 `Missingcontent` 错误。 + +请求体: +```json +{ + "operatorId": "{OPERATOR_ID}", + "content": "# 新标题\n\n新的正文内容,支持 Markdown 格式。", + "contentType": "markdown" +} +``` + +> ⚠️ 此操作**全量覆盖**文档内容,不可撤销。 + +--- + +## 10. 追加文本到段落 + +``` +POST https://api.dingtalk.com/v1.0/doc/suites/documents/{docKey}/blocks/{blockId}/paragraph/appendText +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +请求体: +```json +{ "operatorId": "{OPERATOR_ID}", "text": "追加的文字" } +``` + +--- + +## 11. 添加文档成员 + +``` +POST https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +请求体: +```json +{ + "operatorId": "{OPERATOR_ID}", + "members": [ + { "id": "", "roleType": "viewer" } + ] +} +``` + +| 参数 | 说明 | +|---|---| +| `id` | 用户 userId(**注意**:这里用 userId,不是 unionId)| +| `roleType` | `viewer`(只读)| `editor`(可编辑)| + +--- + +## 12. 更新文档成员权限 + +``` +PUT https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members/{memberId} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +请求体: +```json +{ "operatorId": "{OPERATOR_ID}", "roleType": "viewer" } +``` + +--- + +## 13. 移除文档成员 + +``` +DELETE https://api.dingtalk.com/v1.0/doc/workspaces/{workspaceId}/docs/{nodeId}/members/{memberId}?operatorId={OPERATOR_ID} +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +--- + +## 14. 搜索文档(用于找回可读写 ID) + +``` +GET https://api.dingtalk.com/v1.0/doc/docs?operatorId={OPERATOR_ID}&workspaceId={workspaceId}&keyword={keyword}&maxResults=20&nextToken= +Header: x-acs-dingtalk-access-token: {NEW_TOKEN} +``` + +说明: +- 需要权限:`Document.WorkspaceDocument.Read` +- 返回结果中的 `docs[].nodeBO.nodeId` 即可用于 `/v1.0/doc/suites/documents/{id}` 的读写(该值是 `dentryUuid` 风格 ID) + +示例响应(节选): +```json +{ + "docs": [ + { + "nodeBO": { + "name": "测试创建.adoc", + "nodeId": "np9zOoBVBYwB2OZBIn4y0G1vW1DK0g6l", + "url": "https://alidocs.dingtalk.com/i/nodes/np9zOoBVBYwB2OZBIn4y0G1vW1DK0g6l" + }, + "workspaceBO": { + "workspaceId": "QXvd5SLN2AxOQz0Z" + } + } + ], + "hasMore": false +} +``` + +--- + +## 错误码 + +| HTTP状态码 | 错误码 | 说明 | 处理建议 | +|---|---|---|---| +| 400 | `MissingoperatorId` | operatorId 未传 | 补充 operatorId(unionId)| +| 400 | `paramError` | 参数类型错误 | operatorId 必须是 unionId,不是 userId | +| 403 | `Forbidden.AccessDenied.AccessTokenPermissionDenied` | 应用缺少权限 | 错误中有 `requiredScopes`,开通对应权限 | +| 404 | `InvalidAction.NotFound` | 接口路径不存在 | 检查版本号(v1.0/v2.0)和路径 | +| 429 | — | QPS 限流 | 1 秒后重试 | + +--- + +## 所需应用权限 + +| 功能 | 权限 scope | +|---|---| +| 查询知识库/节点 | `Wiki.Node.Read` | +| 读取文档正文 | `Storage.File.Read` | +| 写入文档正文 | `Storage.File.Write` | +| 创建/删除文档 | `Storage.File.Write` | +| 查询用户 unionId | `Contact.User.Read` | diff --git a/skills/dingtalk-document/scripts/dt_helper.sh b/skills/dingtalk-document/scripts/dt_helper.sh new file mode 100755 index 0000000..e7b2512 --- /dev/null +++ b/skills/dingtalk-document/scripts/dt_helper.sh @@ -0,0 +1,381 @@ +#!/bin/bash +# ============================================================================= +# dt_helper.sh — 钉钉开放平台辅助工具 +# 路径: scripts/common/dt_helper.sh +# 用法: bash scripts/common/dt_helper.sh <命令> [参数] +# ============================================================================= + +set -e + +CONFIG="${DINGTALK_CONFIG:-$HOME/.dingtalk-skills/config}" + +# ───────────────────────────────────────────────────────────────────────────── +# 帮助信息 +# ───────────────────────────────────────────────────────────────────────────── +show_help() { + cat <<'EOF' +钉钉开放平台辅助工具 (dt_helper.sh) +用法: bash scripts/common/dt_helper.sh <命令> [参数] + +Token 管理(两种 token 互不兼容,按域名区分): + --token [--nocache] 获取新版 accessToken(用于 api.dingtalk.com 域名的所有接口) + 适用:待办、文档、AI 表格等 api.dingtalk.com 域名下所有版本的接口 + 请求头:x-acs-dingtalk-access-token: + 有缓存且未过期则直接返回,否则自动刷新并缓存 + --nocache:跳过缓存,强制重新获取(token 被提前吊销时使用) + --token-info 查看新版 token 缓存状态(是否有效、剩余有效秒数) + --clear-token 清除缓存的新版 token(下次 --token 时强制重新获取) + --old-token [--nocache] + 获取旧版 access_token(用于 oapi.dingtalk.com 域名的所有接口) + 适用:群消息/工作通知/userId↔unionId 转换等 oapi.dingtalk.com 接口 + 不适用:api.dingtalk.com 接口(如待办、文档、AI表格) + ⚠️ 新旧两种 token 互不兼容,混用会导致 401/403 + --nocache:跳过缓存,强制重新获取(token 被提前吊销时使用) + +身份转换: + --to-unionid [userId] 将 userId 转换为 unionId + 不传参数:转换配置中的 DINGTALK_MY_USER_ID(操作者自身), + 结果首次自动写入 DINGTALK_MY_OPERATOR_ID + 传入参数:动态转换指定 userId,仅返回结果,不写入配置 + --to-userid [unionId] 将 unionId 反向转换为 userId(需传入参数) + +配置管理: + --config 查看 ~/.dingtalk-skills/config 中的所有配置项(敏感项脱敏显示) + --get KEY [KEY...] 获取一个或多个配置项的值(敏感项脱敏显示) + --set KEY=VALUE 将配置项持久化写入配置文件(已存在则更新,不存在则追加,目录自动创建) + +帮助: + --help, -h 显示此帮助信息 + +环境变量: + DINGTALK_CONFIG 覆盖默认配置文件路径(默认 ~/.dingtalk-skills/config) + +配置文件: + ~/.dingtalk-skills/config key=value 格式,存储以下键: + DINGTALK_APP_KEY 应用 Client ID(AppKey) + DINGTALK_APP_SECRET 应用 Client Secret(AppSecret) + DINGTALK_MY_USER_ID 企业员工 ID(userId,管理后台通讯录可查) + DINGTALK_MY_OPERATOR_ID 操作者 unionId(由 --to-unionid 自动生成) + DINGTALK_ACCESS_TOKEN 新版 token 缓存 + DINGTALK_TOKEN_EXPIRY 新版 token 过期时间戳(Unix 秒) + DINGTALK_OLD_TOKEN 旧版 token 缓存 + DINGTALK_OLD_TOKEN_EXPIRY 旧版 token 过期时间戳(Unix 秒) + +EOF +} + +# ───────────────────────────────────────────────────────────────────────────── +# 工具函数 +# ───────────────────────────────────────────────────────────────────────────── + +# 从配置文件读取指定键的值 +cfg_get() { + local key="$1" + grep "^${key}=" "$CONFIG" 2>/dev/null | head -1 | cut -d= -f2- +} + +# 写入或更新配置文件中的键值 +cfg_set() { + local key="$1" + local value="$2" + mkdir -p "$(dirname "$CONFIG")" + touch "$CONFIG" + if grep -q "^${key}=" "$CONFIG" 2>/dev/null; then + sed -i "s|^${key}=.*|${key}=${value}|" "$CONFIG" + else + echo "${key}=${value}" >> "$CONFIG" + fi +} + +# 从配置文件删除指定键 +cfg_del() { + local key="$1" + sed -i "/^${key}=/d" "$CONFIG" 2>/dev/null || true +} + +# 确保必须的配置项存在,否则报错退出 +require_cfg() { + local key="$1" + local val + val=$(cfg_get "$key") + if [ -z "$val" ]; then + echo "❌ 缺少配置项 ${key},请先运行: bash scripts/common/dt_helper.sh --set ${key}=<值>" >&2 + exit 1 + fi + echo "$val" +} + +# ───────────────────────────────────────────────────────────────────────────── +# Token 管理 +# ───────────────────────────────────────────────────────────────────────────── + +cmd_token() { + local force="${1:-}" app_key app_secret cached expiry now resp token expire_in + + app_key=$(require_cfg DINGTALK_APP_KEY) + app_secret=$(require_cfg DINGTALK_APP_SECRET) + now=$(date +%s) + + if [ "$force" != "--nocache" ]; then + cached=$(cfg_get DINGTALK_ACCESS_TOKEN) + expiry=$(cfg_get DINGTALK_TOKEN_EXPIRY) + if [ -n "$cached" ] && [ -n "$expiry" ] && [ "$now" -lt "$expiry" ]; then + echo "$cached" + return 0 + fi + fi + + # 过期或无缓存,重新获取 + resp=$(curl -s -X POST "https://api.dingtalk.com/v1.0/oauth2/accessToken" \ + -H "Content-Type: application/json" \ + -d "{\"appKey\":\"${app_key}\",\"appSecret\":\"${app_secret}\"}") + + token=$(echo "$resp" | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4) + expire_in=$(echo "$resp" | grep -o '"expireIn":[0-9]*' | cut -d: -f2) + + if [ -z "$token" ]; then + echo "❌ 获取 token 失败: $resp" >&2 + exit 1 + fi + + cfg_set DINGTALK_ACCESS_TOKEN "$token" + cfg_set DINGTALK_TOKEN_EXPIRY "$((now + expire_in - 200))" + + echo "$token" +} + +cmd_token_info() { + local cached expiry now remaining + + cached=$(cfg_get DINGTALK_ACCESS_TOKEN) + expiry=$(cfg_get DINGTALK_TOKEN_EXPIRY) + now=$(date +%s) + + if [ -z "$cached" ]; then + echo "状态: 无缓存(从未获取或已清除)" + return 0 + fi + + if [ -z "$expiry" ] || [ "$now" -ge "$expiry" ]; then + echo "状态: 已过期" + echo "Token: ${cached:0:20}..." + else + remaining=$((expiry - now)) + echo "状态: 有效" + echo "Token: ${cached:0:20}..." + echo "剩余: ${remaining} 秒(约 $((remaining / 60)) 分钟)" + fi +} + +cmd_clear_token() { + cfg_del DINGTALK_ACCESS_TOKEN + cfg_del DINGTALK_TOKEN_EXPIRY + echo "✅ 新版 Token 缓存已清除" +} + +cmd_old_token() { + # 旧版 access_token,用于所有 oapi.dingtalk.com 接口: + # - 群消息、工作通知、互动卡片(dingtalk-message) + # - userId ↔ unionId 转换 + # ⚠️ 不可用于 api.dingtalk.com 接口(待办、文档、AI表格等) + local force="${1:-}" app_key app_secret resp token cached expiry now + + app_key=$(require_cfg DINGTALK_APP_KEY) + app_secret=$(require_cfg DINGTALK_APP_SECRET) + now=$(date +%s) + + if [ "$force" != "--nocache" ]; then + cached=$(cfg_get DINGTALK_OLD_TOKEN) + expiry=$(cfg_get DINGTALK_OLD_TOKEN_EXPIRY) + if [ -n "$cached" ] && [ -n "$expiry" ] && [ "$now" -lt "$expiry" ]; then + echo "$cached" + return 0 + fi + fi + + resp=$(curl -s "https://oapi.dingtalk.com/gettoken?appkey=${app_key}&appsecret=${app_secret}") + token=$(echo "$resp" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4) + expires_in=$(echo "$resp" | grep -o '"expires_in":[0-9]*' | cut -d: -f2) + + if [ -z "$token" ]; then + echo "❌ 获取旧版 token 失败: $resp" >&2 + exit 1 + fi + + cfg_set DINGTALK_OLD_TOKEN "$token" + cfg_set DINGTALK_OLD_TOKEN_EXPIRY "$((now + expires_in - 200))" + + echo "$token" +} + +# ───────────────────────────────────────────────────────────────────────────── +# 身份转换 +# ───────────────────────────────────────────────────────────────────────────── + +cmd_to_unionid() { + local user_id="$1" + local is_self=false + local old_token resp union_id + + # 未传参 → 使用配置中的操作者自身 userId,转换结果写入配置 + if [ -z "$user_id" ]; then + user_id=$(require_cfg DINGTALK_MY_USER_ID) + is_self=true + fi + + old_token=$(cmd_old_token) + + resp=$(curl -s -X POST \ + "https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${old_token}" \ + -H "Content-Type: application/json" \ + -d "{\"userid\":\"${user_id}\"}") + + # 注意:使用无下划线的 unionid 字段(有下划线的 union_id 可能为空) + union_id=$(echo "$resp" | grep -o '"unionid":"[^"]*"' | head -1 | cut -d'"' -f4) + + if [ -z "$union_id" ]; then + echo "❌ userId→unionId 转换失败: $resp" >&2 + exit 1 + fi + + # 仅当转换的是操作者自身时,才写入配置(动态转换他人 userId 不写入) + if "$is_self" && [ -z "$(cfg_get DINGTALK_MY_OPERATOR_ID)" ]; then + cfg_set DINGTALK_MY_OPERATOR_ID "$union_id" + echo "✅ 自身 unionId 已写入配置 DINGTALK_MY_OPERATOR_ID" >&2 + fi + + echo "$union_id" +} + +cmd_to_userid() { + local union_id="$1" + local old_token resp user_id + + if [ -z "$union_id" ]; then + echo "❌ 请提供 unionId 参数" >&2 + exit 1 + fi + + old_token=$(cmd_old_token) + + resp=$(curl -s -X POST \ + "https://oapi.dingtalk.com/topapi/user/getbyunionid?access_token=${old_token}" \ + -H "Content-Type: application/json" \ + -d "{\"unionid\":\"${union_id}\"}") + + user_id=$(echo "$resp" | grep -o '"userid":"[^"]*"' | head -1 | cut -d'"' -f4) + + if [ -z "$user_id" ]; then + echo "❌ unionId→userId 转换失败: $resp" >&2 + exit 1 + fi + + echo "$user_id" +} + +# ───────────────────────────────────────────────────────────────────────────── +# 配置管理 +# ───────────────────────────────────────────────────────────────────────────── + +cmd_config() { + if [ ! -f "$CONFIG" ]; then + echo "配置文件不存在: $CONFIG" + echo "使用 --set KEY=VALUE 写入配置项" + return 0 + fi + + echo "配置文件: $CONFIG" + echo "─────────────────────────────────" + # 脱敏显示 SECRET 和 TOKEN + while IFS= read -r line; do + key="${line%%=*}" + val="${line#*=}" + case "$key" in + DINGTALK_APP_SECRET|DINGTALK_ACCESS_TOKEN|DINGTALK_OLD_TOKEN) + echo "${key}=${val:0:6}***(已脱敏)" + ;; + *) + echo "$line" + ;; + esac + done < "$CONFIG" +} + +cmd_get() { + if [ $# -eq 0 ]; then + echo "❌ 请提供至少一个键名,用法: --get KEY [KEY2 ...]" >&2 + exit 1 + fi + for key in "$@"; do + val=$(cfg_get "$key") + if [ -z "$val" ]; then + echo "${key}=(未设置)" + else + case "$key" in + DINGTALK_APP_SECRET|DINGTALK_ACCESS_TOKEN|DINGTALK_OLD_TOKEN) + echo "${key}=${val:0:6}***(脱敏)" + ;; + *) + echo "${key}=${val}" + ;; + esac + fi + done +} + +cmd_set() { + local kv="$1" + if [ -z "$kv" ] || [[ "$kv" != *"="* ]]; then + echo "❌ 格式错误,用法: --set KEY=VALUE" >&2 + exit 1 + fi + local key="${kv%%=*}" + local value="${kv#*=}" + cfg_set "$key" "$value" + echo "✅ 已设置 ${key}" +} + +# ───────────────────────────────────────────────────────────────────────────── +# 入口:解析命令 +# ───────────────────────────────────────────────────────────────────────────── + +CMD="${1:-}" + +case "$CMD" in + --help|-h|"") + show_help + ;; + --token) + cmd_token "${2:-}" + ;; + --token-info) + cmd_token_info + ;; + --clear-token) + cmd_clear_token + ;; + --old-token) + cmd_old_token "${2:-}" + ;; + --to-unionid) + cmd_to_unionid "${2:-}" + ;; + --to-userid) + cmd_to_userid "${2:-}" + ;; + --config) + cmd_config + ;; + --get) + shift + cmd_get "$@" + ;; + --set) + cmd_set "${2:-}" + ;; + *) + echo "❌ 未知命令: $CMD" >&2 + echo "运行 --help 查看用法" >&2 + exit 1 + ;; +esac diff --git a/skills/dingtalk-log/SKILL.md b/skills/dingtalk-log/SKILL.md new file mode 100644 index 0000000..51365bf --- /dev/null +++ b/skills/dingtalk-log/SKILL.md @@ -0,0 +1,86 @@ +# 钉钉工作日志查询 + +查询钉钉工作日志(周报/日报等)。 + +## 接口信息 + +- **接口地址**: `POST https://oapi.dingtalk.com/topapi/report/list` +- **认证方式**: access_token(基于企业内部应用) +- **时间单位**: ⚠️ **毫秒**(不是秒!) + +## 前置条件 + +1. 获取 access_token: +```bash +curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \ + -H 'Content-Type: application/json' \ + -d '{ + "appKey": "dingklemniq8uqk5qbgx", + "appSecret": "_8EHgyhvHRHRMx6fZbh9LNpQoxyYl3At0b-fXXlQiahwupbt9oY5P6Grj8IM9Dx8" + }' +``` + +2. 凭证存储在 `TOOLS.md`: + - AgentId: 4404185308 + - Client ID (AppKey): dingklemniq8uqk5qbgx + - Client Secret (AppSecret): _8EHgyhvHRHRMx6fZbh9LNpQoxyYl3At0b-fXXlQiahwupbt9oY5P6Grj8IM9Dx8 + +## 调用示例 + +```bash +# 查询指定用户的日志(时间单位:毫秒) +curl -s 'https://oapi.dingtalk.com/topapi/report/list?access_token={TOKEN}' \ + -H 'Content-Type: application/json' \ + -d '{ + "userid": "121922510028034588", + "offset": 0, + "size": 10, + "start_time": 1738329600000, + "end_time": 1743292800000, + "cursor": 0 + }' +``` + +## 参数说明 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| userid | string | ✅ | 用户的钉钉 userid | +| start_time | number | ✅ | 开始时间,**毫秒**时间戳 | +| end_time | number | ✅ | 结束时间,**毫秒**时间戳 | +| cursor | number | ✅ | 分页游标(首次查询传 0) | +| size | number | ✅ | 每页数量(建议 10) | +| offset | number | ❌ | 偏移量(兼容旧版,可不传) | + +## 返回字段说明 + +日志内容在 `result.data_list` 数组中,每个元素的 `contents` 数组包含各字段: + +| contents.key | 说明 | +|-------------|------| +| 本周完成工作 / 本日完成工作 | 完成事项 | +| 本周工作总结 / 本日工作总结 | 详细总结 | +| 下周工作计划 / 下日工作计划 | 后续计划 | +| 需协调与帮助 | 协调事项 | +| 图片 | 图片列表 | +| 附件 | 附件列表 | + +其他字段: +- `report_id`: 日志ID +- `template_name`: 日志模板名称(如"经理人周报") +- `creator_name`: 创建人姓名 +- `dept_name`: 部门名称 +- `create_time`: 创建时间(毫秒) + +## 分页查询 + +通过 `cursor` 分页: +1. 首次查询 `cursor: 0` +2. 返回 `next_cursor` 作为下次查询的游标 +3. `has_more: false` 表示最后一页 + +## 注意事项 + +1. ⚠️ **时间单位必须是毫秒**:如 `1743292800000`(不能用 `1743292800`) +2. ⚠️ **字段名是 `userid`(全小写)**,不是 `userId` +3. 部分日志内容可能超长被截断,返回可能不完整 diff --git a/skills/dingtalk-log/scripts/dt_log.sh b/skills/dingtalk-log/scripts/dt_log.sh new file mode 100644 index 0000000..bc51333 --- /dev/null +++ b/skills/dingtalk-log/scripts/dt_log.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# 钉钉工作日志查询脚本 + +# 配置(从 TOOLS.md 读取) +APP_KEY="dingklemniq8uqk5qbgx" +APP_SECRET="_8EHgyhvHRHRMx6fZbh9LNpQoxyYl3At0b-fXXlQiahwupbt9oY5P6Grj8IM9Dx8" +API_BASE="https://oapi.dingtalk.com" + +# 获取 access_token +get_token() { + curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \ + -H 'Content-Type: application/json' \ + -d "{\"appKey\":\"$APP_KEY\",\"appSecret\":\"$APP_SECRET\"}" | \ + grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4 +} + +# 查询日志 +query_logs() { + local userid=$1 + local start_time=$2 # 毫秒 + local end_time=$3 # 毫秒 + local size=${4:-10} + local cursor=${5:-0} + local token=$6 + + curl -s "$API_BASE/topapi/report/list?access_token=$token" \ + -H 'Content-Type: application/json' \ + -d "{ + \"userid\": \"$userid\", + \"start_time\": $start_time, + \"end_time\": $end_time, + \"cursor\": $cursor, + \"size\": $size + }" +} + +# 解析毫秒时间戳为日期 +ms_to_date() { + local ms=$1 + date -d @$((ms / 1000)) "+%Y-%m-%d %H:%M:%S" +} + +# 帮助信息 +usage() { + echo "钉钉工作日志查询" + echo "" + echo "用法: $0 [size] [cursor]" + echo "" + echo "示例:" + echo " $0 121922510028034588 1738329600000 1743292800000" + echo " $0 121922510028034588 1738329600000 1743292800000 20 0" + echo "" + echo "时间戳获取: date +%s000000 (mac/linux)" +}