金山工时填报自动化 Skill
触发场景
用户需要向金山文档(WPS 云文档 / 轻维表 / 多维表格)的工时填报表单录入数据,且表单具备以下特征:
- 表单由「关联记录」字段(填报人 / 填报项目)驱动,点开后是 Canvas 虚拟表格(DOM 点击无效,必须用坐标点击);
- 含文本域(工作内容描述),且为 React 受控组件(直接赋值不生效,须用原生 setter + 事件派发);
- 其余字段(日期、工作类型、工作地点、工时)多为预填默认值。
用户的极简指令形如:
> 「帮我填报工时,填报人=赵杰,日期=今天,计划=https://docs.wps.cn/l/xxxx,项目=ZC数智董事会平台建设」
本 skill 只接收 5 个输入,其余全部自动推断/使用默认配置(见下方「输入参数」与「默认配置区」)。
依赖
- web-access skill:提供 CDP Proxy(localhost:3456)与浏览器操作能力。使用前必须先加载 web-access 并完成其前置检查(
node <web-access>/scripts/check-deps.mjs)。本 skill 不重复实现浏览器桥接,所有/eval、/click、/screenshot、/new、/close调用均走 web-access 的 Proxy API(详见web-access/references/cdp-api.md)。 - kdocs 连接器(MCP):用于读取计划文档(
mcp__kdocs__read_file)。若未连接,提示用户在设置中启用「金山文档」连接器。
默认配置区(首次部署时填写,之后通常不变)
把这些常量写进本 skill 的记忆或直接硬编码在流程里。这是该技能「一句话触发」的关键——用户不必每次重复。
FORM_ENTRY_URL: "https://www.kdocs.cn/office/d/517879642607?share_id=F0G7nqHnG64eGq&R=L1MvNTY=" # 工时填报表单直链:打开即进入填报表单,无需再点「前往」
DEFAULT_WORK_TYPE: "实施" # 工作类型默认值
DEFAULT_LOCATION: "ZC信息公司" # 工作地点默认值
DEFAULT_HOURS: 8 # 填报工时默认值
# 以下字段如表单中各次相同可固定;若不同则由用户输入覆盖
ROLE_PERSPECTIVE: "项目经理" # 生成工作内容时采用的角色视角
> 说明:以上为 ZC 项目示例值。FORM_ENTRY_URL 与三个默认值若长期不变,建议直接写死;角色视角用于生成「工作内容描述」。
> FORM_ENTRY_URL 现为表单直链,打开即进入填报表单,无需再从中枢页点「前往」(原中枢页 share_id=G09kKqkG4kSVgh 在只读视图下「前往」按钮无响应,已弃用)。
输入参数(用户每次需提供的)
| 参数 | 含义 | 来源 |
|---|---|---|
填报人 |
表单「填报人」关联字段要选中的人名 | 用户给出 |
日期 |
填报日期,「今天」则取当前日期 | 用户给出 |
计划文档地址 |
用于生成工作内容的项目整体计划文档 URL(ksheet/文档) | 用户给出 |
填报项目 |
表单「填报项目」关联字段要选中的项目名 | 用户给出 |
填报入口 |
工时填报表单入口 URL | 默认取 FORM_ENTRY_URL,用户可覆盖 |
其余字段:工作类型=DEFAULT_WORK_TYPE、工作地点=DEFAULT_LOCATION、工时=DEFAULT_HOURS、工作内容=按计划自动生成。
执行流程(六步)
步骤 0:前置检查 + 打开表单
# 1) 确保 web-access 的 CDP Proxy 在运行(遵循 web-access 指引完成 check-deps)
# 2) 打开填报表单直链到新后台 tab(FORM_ENTRY_URL 即表单本身,打开即进入填报表单)
curl -s -X POST --data-raw '<FORM_ENTRY_URL>' http://localhost:3456/new
# 返回 { targetId },记为 TARGET
sleep 4 # 等待填报表单渲染
# 可选:截图确认已进入填报表单
curl -s "http://localhost:3456/screenshot?target=TARGET&file=C:/Users/HW/AppData/Local/Temp/timesheet_form.png"
> 必须保留用户原有 tab,所有操作在自建后台 tab(TARGET)中进行;结束用 /close?target=TARGET 关闭。
> FORM_ENTRY_URL 已是表单直链,无需再点「前往」;若打开后非填报表单而是中枢页,再回退到「在『外包工时填报』行点击『前往』」的旧逻辑。
步骤 1:读取计划文档 → 生成工作内容描述
# 通过 kdocs 连接器读取计划(markdown 格式)
# 工具:mcp__kdocs__read_file (参数 url=计划文档地址, format=markdown)
返回结构为 data.content.range_data.detail.rangeData(数组,元素含 originRow / originCol / originalCellValue)。按行聚合:
// 伪代码:转成 [[col0,col1,...], ...] 的二维表
const rows = {};
cells.forEach(c => { (rows[c.originRow] ??= {})[c.originCol] = c.originalCellValue; });
生成规则(关键):从计划中提取
1. 项目当前阶段(如「第3阶段:系统开发」)与阶段状态;
2. 当前阶段下的关键模块 / 任务 / 里程碑;
3. 填报人角色(默认项目经理)在该阶段通常负责的事(进度跟进、跨团队协调、测试审核、会议组织)。
据此生成一段「实施类、项目经理视角」的工作内容,格式:
【<填报项目>】<工作类型>工作:1.跟进<当前阶段>各功能模块开发进度,重点包括<模块A、模块B…>;2.协调开发团队解决集成开发与接口对接问题;3.审核功能单元测试用例并跟踪测试问题;4.推进系统测试方案制定与测试环境准备;5.组织项目周会,汇报进展并协调资源。
保持 3–6 条、具体、贴合计划原文,不要编造计划中没有的模块。
步骤 2:识别表单结构
curl -s -X POST "http://localhost:3456/eval?target=TARGET" --data-binary @- <<'JSEOF'
(function(){
var items = document.querySelectorAll('.form-field-item');
var out = [];
items.forEach((it,i)=>{
out.push({ i, label: it.textContent.trim().substring(0,20),
hasTrigger: !!it.querySelector('.form-list-input, input, textarea') });
});
return JSON.stringify(out);
})()
JSEOF
确认字段顺序与类型。记录:
- 日期框(多为预填当天)
- 填报人 → 关联记录(form-field-item 内含 .form-list-input 触发)
- 填报项目 → 关联记录
- 工作类型 / 工作地点(多为预填默认,若未预填则同为关联记录,走步骤 3 同法)
- 工作内容描述 → textarea.kd-textarea-inner
- 填报工时 → 数字框(多为预填 8)
步骤 3:填写「关联记录」字段(填报人 / 填报项目)—— 核心难点
关联记录点开后弹窗 .db-records-selector 内是一张 Canvas 虚拟表格,DOM 选择器点击行无效,必须用坐标 + MouseEvent 模拟真实点击。
通用点击函数(在 /eval 中执行):
function selectInCanvas(targetText, xColumnOffset){
// 1) 打开字段弹窗(按 label 找触发元素)
var items = document.querySelectorAll('.form-field-item');
for (var i=0;i<items.length;i++){
if (items[i].textContent.includes('<字段标签,如 填报人>')){
var trig = items[i].querySelector('.form-list-input'); trig && trig.click();
break;
}
}
// 2) 若弹窗有搜索框,先输入目标文本缩小范围(强烈推荐,可避免坐标错位)
var box = document.querySelector('.kd-dialog input, .db-records-selector input');
if (box){ box.focus(); box.value='<目标文本>'; box.dispatchEvent(new Event('input',{bubbles:true})); }
// 3) 计算目标行坐标并点击
var cv = document.querySelector('.db-records-selector canvas');
var r = cv.getBoundingClientRect();
// 表头约 40px,首数据行 y≈r.y+55;行高约 28px(搜索后目标常为唯一行)
var x = r.x + (xColumnOffset||60);
var y = r.y + 55;
['mousedown','mouseup','click'].forEach(t=>cv.dispatchEvent(new MouseEvent(t,{
clientX:x, clientY:y, button:0, bubbles:true, cancelable:true, view:window, detail:1
})));
return 'clicked@'+Math.round(x)+','+Math.round(y);
}
> 校正方法:点击后立即 /screenshot,肉眼(或视觉识别)确认高亮行是否为目标。若偏一行,y 增减 28px 重点一次即可命中。搜索过滤后目标通常唯一,命中率高。
确认选择:选中目标行后点击弹窗确定按钮
curl -s -X POST "http://localhost:3456/click?target=TARGET" -d '.db-records-selector .kd-button-primary'
对「填报人」「填报项目」分别执行上述流程(项目名通常首页即命中,可不搜索直接点首行)。
步骤 4:填写文本域(工作内容描述)—— React 受控组件
直接 el.value=... 不生效,必须用原生 setter 写入再派发事件:
curl -s -X POST "http://localhost:3456/eval?target=TARGET" --data-binary @- <<'JSEOF'
(function(){
var ta = document.querySelector('.kd-textarea-inner');
if(!ta) return JSON.stringify({error:'no textarea'});
var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set;
setter.call(ta, '<步骤1生成的工作内容>');
ta.dispatchEvent(new Event('input',{bubbles:true}));
ta.dispatchEvent(new Event('change',{bubbles:true}));
return JSON.stringify({filled:true, len: ta.value.length});
})()
JSEOF
> 数字框(工时)如未预填,同法用 HTMLInputElement.prototype 的 setter 写入。
步骤 5:截图核对(不自动提交)
curl -s "http://localhost:3456/screenshot?target=TARGET&file=C:/Users/HW/AppData/Local/Temp/timesheet_preview.png"
将截图交给用户核对:填报人 / 项目 / 日期 / 工作类型 / 地点 / 工时 / 工作内容是否无误。默认不提交,等待用户明确说「提交」。
步骤 6:提交(用户确认后)
# 找到提交按钮
curl -s -X POST "http://localhost:3456/eval?target=TARGET" -d \
'JSON.stringify([...document.querySelectorAll("button")].filter(b=>b.textContent.includes("提交")).map(b=>({t:b.textContent.trim()})))'
# 点击(先用 /click 按选择器,失败退回坐标)
curl -s -X POST "http://localhost:3456/click?target=TARGET" -d '.form-submit-btn'
sleep 2
curl -s "http://localhost:3456/screenshot?target=TARGET&file=C:/Users/HW/AppData/Local/Temp/timesheet_done.png"
截图应显示「提交成功」。最后 curl -s "http://localhost:3456/close?target=TARGET" 关闭自建 tab。
关键技术要点(踩坑总结)
- 关联记录 = Canvas 虚拟表格:弹窗
.db-records-selector内的行是 Canvas 绘制的,任何 DOM 点击 /el.click()都不生效。唯一可靠方式是getBoundingClientRect()拿 canvas 坐标 → 算目标行 y(首行 ≈ r.y+55,行高 ≈28px)→ 在 canvas 上派发mousedown/mouseup/click序列。先搜索过滤再点,配合截图校正。 - React 受控输入:文本域/数字框直接赋值会被框架忽略。必须
Object.getOwnPropertyDescriptor(原型,'value').set写入,再dispatchEvent(new Event('input'/'change'))。 - 坐标点击的确定性:用
/eval派发 MouseEvent 比/clickAt选择器更可控(canvas 无独立行元素);但真实手势场景(文件对话框等)仍用 web-access 的/clickAt。 - 默认字段优先信任预填:日期/工作类型/地点/工时若表单已预填正确值,不要改动,只处理 填报人 / 填报项目 / 工作内容 三处。
安全约定
- 表单涉及个人工时数据,默认填完截图交用户确认,不自动提交。仅在用户明确回复「提交」后执行步骤 6。
- 不关闭、不干扰用户原有 tab;结束后关闭自建 tab。
- 浏览器自动化有账号风控风险,沿用 web-access 的须知提示。
参考
references/kdocs-timesheet.md:完整可复制代码片段与字段选择器清单、ZC 项目实操记录。web-access/references/cdp-api.md:CDP Proxy 全部 API 端点。web-access/SKILL.md:浏览器自动化总指引与赏析哲学。