@ecc-hgy/ae 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ecc-hgy/ae)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
Agentic Engineering is a lightweight personal software lifecycle method: SDD records what to build, TDD drives how to prove it works. It is delivered as a zero-dependency npm CLI plus agent skills for
|
|
6
|
+
Agentic Engineering is a lightweight personal software lifecycle method: SDD records what to build, TDD drives how to prove it works. It is delivered as a zero-dependency npm CLI plus agent skills for multiple AI coding platforms.
|
|
7
7
|
|
|
8
8
|
## How It Works
|
|
9
9
|
|
|
@@ -36,10 +36,11 @@ npx @ecc-hgy/ae setup
|
|
|
36
36
|
npx @ecc-hgy/ae init "one-line project goal"
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`ae setup` installs reusable method assets:
|
|
39
|
+
`ae setup` installs reusable method assets into the AI coding platforms you select. In interactive terminals it prompts for platforms and preselects detected local platform directories. In scripts or CI, pass platforms explicitly:
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
-
|
|
41
|
+
```bash
|
|
42
|
+
npx @ecc-hgy/ae setup --platform claude,codex
|
|
43
|
+
```
|
|
43
44
|
|
|
44
45
|
`ae init` creates the platform-neutral project skeleton and renders entry files. Project-specific rules live directly in `AGENTS.md`.
|
|
45
46
|
|
|
@@ -47,6 +48,7 @@ npx @ecc-hgy/ae init "one-line project goal"
|
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
50
|
ae setup
|
|
51
|
+
ae setup --platform claude,codex,cursor
|
|
50
52
|
ae init "one-line project goal"
|
|
51
53
|
ae --help
|
|
52
54
|
ae --version
|
|
@@ -66,10 +68,25 @@ Both commands are idempotent. They create missing files, skip existing files, an
|
|
|
66
68
|
|
|
67
69
|
Current target platforms:
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
| Platform | ID | Skills directory |
|
|
72
|
+
|----------|----|------------------|
|
|
73
|
+
| Claude Code | `claude` | `.claude/skills/` |
|
|
74
|
+
| Codex | `codex` | `.codex/skills/` |
|
|
75
|
+
| Windsurf | `windsurf` | `.windsurf/skills/` |
|
|
76
|
+
| GitHub Copilot | `github-copilot` | `.github/skills/` |
|
|
77
|
+
| Kilo Code | `kilocode` | `.kilocode/skills/` |
|
|
78
|
+
| Kimi Code | `kimicode` | `.kimi-code/skills/` |
|
|
79
|
+
| CodeBuddy | `codebuddy` | `.codebuddy/skills/` |
|
|
80
|
+
| Qoder | `qoder` | `.qoder/skills/` |
|
|
81
|
+
| Trae | `trae` | `.trae/skills/` |
|
|
82
|
+
| Cursor | `cursor` | `.cursor/skills/` |
|
|
83
|
+
| OpenCode | `opencode` | `.opencode/skills/` |
|
|
84
|
+
| Antigravity | `antigravity` | `.agents/skills/` |
|
|
85
|
+
| Gemini CLI | `gemini` | `.gemini/skills/` |
|
|
86
|
+
| Qwen Code | `qwen` | `.qwen/skills/` |
|
|
87
|
+
| Kiro | `kiro` | `.kiro/skills/` |
|
|
88
|
+
|
|
89
|
+
The first implementation is project-scoped only. It does not generate platform-specific entry rule files such as `.cursorrules`, `GEMINI.md`, or Copilot instructions.
|
|
73
90
|
|
|
74
91
|
## Method Overview
|
|
75
92
|
|
|
@@ -110,7 +127,7 @@ The two hard gates are:
|
|
|
110
127
|
- [Spec Framework](docs/spec-framework.md)
|
|
111
128
|
- [S0 Bootstrap](docs/scenarios/s0-bootstrap.md)
|
|
112
129
|
- [S1 Main Loop](docs/scenarios/s1-main-loop.md)
|
|
113
|
-
- [Skill Candidate Design Notes](
|
|
130
|
+
- [Skill Candidate Design Notes](方案/skill-candidates/README.md)
|
|
114
131
|
|
|
115
132
|
## Repository Layout
|
|
116
133
|
|
|
@@ -119,8 +136,8 @@ bin/ npm executable entry
|
|
|
119
136
|
src/ zero-dependency CLI implementation
|
|
120
137
|
skills/ Selected Agentic Engineering skills
|
|
121
138
|
templates/ Entry file templates
|
|
122
|
-
docs/
|
|
123
|
-
|
|
139
|
+
docs/ Methodology docs (framework, scenarios, spec framework)
|
|
140
|
+
方案/ Design & decision records (issues, roadmap, skill candidates, archive)
|
|
124
141
|
tests/ node:test coverage for CLI behavior
|
|
125
142
|
```
|
|
126
143
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: powerautomate-email-to-sharepoint-excel
|
|
3
|
+
description: Use when the user needs to collect specific Outlook or Microsoft 365 corporate emails, filter them by sender/subject/body rules, extract mail fields such as title and content, and persist them for later analysis when Codex, local plugins, or direct mailbox APIs cannot access the mailbox. Guides the agent through SharePoint preparation, Power Automate flow creation, and retrieving the captured data from SharePoint Excel.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Outlook 邮件采集到 SharePoint Excel
|
|
7
|
+
|
|
8
|
+
这个 skill 用来指导 agent 在遇到“需要筛选 Outlook 特定邮件并获取内容”的需求时,设计一条可落地的 Microsoft 365 自动化路径。它不是某个单一 case 的记录,而是可复用的实施指南。
|
|
9
|
+
|
|
10
|
+
## 触发判断
|
|
11
|
+
|
|
12
|
+
先确认是否属于这类问题:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
1. 邮箱来源是 Outlook / Microsoft 365 / Exchange Online
|
|
16
|
+
2. 账号是集团或公司账号,权限受租户策略控制
|
|
17
|
+
3. 需要持续获取邮件内容,而不是人工查看一封邮件
|
|
18
|
+
4. Codex、浏览器插件、本地脚本或 Graph API 暂时无法直接访问邮箱
|
|
19
|
+
5. 用户可以使用 Power Automate、SharePoint、OneDrive 或 Excel Online
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
满足这些条件时,优先考虑 Microsoft 365 内部链路:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Outlook 邮件触发
|
|
26
|
+
→ Power Automate 筛选和转换
|
|
27
|
+
→ SharePoint Excel / SharePoint List / OneDrive 文件
|
|
28
|
+
→ 本地同步或脚本分析
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
不要默认尝试新版 Outlook 的本地 COM/VBA 自动化;新版 Outlook 不适合这种路径。企业账号也不要假设 IMAP、Graph、HTTP webhook 一定可用。
|
|
32
|
+
|
|
33
|
+
## 工作分解
|
|
34
|
+
|
|
35
|
+
把任务拆成三部分:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
1. SharePoint 准备工作
|
|
39
|
+
2. Power Automate 创建 Flow
|
|
40
|
+
3. 获取 SharePoint 中的数据用于本地分析
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
当前已沉淀的方法:
|
|
44
|
+
|
|
45
|
+
| 方法 | 输出位置 | 适合情况 |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| 方法 1:Power Automate 写入 SharePoint Excel | SharePoint 文档库中的 Excel Table | 低频邮件日志、轻量分析、用户熟悉 Excel |
|
|
48
|
+
|
|
49
|
+
后续可补充的方法:
|
|
50
|
+
|
|
51
|
+
- Power Automate 写入 SharePoint List。
|
|
52
|
+
- Power Automate 写 JSON/CSV 到 OneDrive 或 SharePoint 文件夹。
|
|
53
|
+
- Microsoft Graph 本地脚本拉取邮件并写入 SQLite/JSONL。
|
|
54
|
+
- Graph webhook + 云端或本地隧道接收服务。
|
|
55
|
+
|
|
56
|
+
## 方法 1 总览
|
|
57
|
+
|
|
58
|
+
目标链路:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
SharePoint Site + 文档库 + Excel Table
|
|
62
|
+
↓
|
|
63
|
+
Power Automate 自动化云端流
|
|
64
|
+
↓
|
|
65
|
+
Office 365 Outlook:新电子邮件到达时(V3)
|
|
66
|
+
↓
|
|
67
|
+
按场景配置邮件筛选规则
|
|
68
|
+
↓
|
|
69
|
+
Html 到文本:清理正文
|
|
70
|
+
↓
|
|
71
|
+
Excel Online (Business):在表中插入新行
|
|
72
|
+
↓
|
|
73
|
+
从 SharePoint/OneDrive 同步或下载数据做本地分析
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## 1. SharePoint 准备工作
|
|
78
|
+
|
|
79
|
+
### 1.1 准备 SharePoint Site
|
|
80
|
+
|
|
81
|
+
确认用户有一个可用的 SharePoint Site,例如:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
https://tenant.sharepoint.com/sites/team-site
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
记录三类信息:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
SharePoint Site URL
|
|
91
|
+
文档库名称,常见为 文档 / Documents / Shared Documents
|
|
92
|
+
Excel 文件名和路径
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
在 Power Automate 的 Excel 动作中,`位置 / Location` 通常要从下拉中选择:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
SharePoint Site - <site name>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
不要优先手填完整 site URL。手填 URL 可能导致后续文档库下拉报 `source 未设置或无效`。
|
|
102
|
+
|
|
103
|
+
### 1.2 准备 SharePoint Excel 文件
|
|
104
|
+
|
|
105
|
+
在 SharePoint 文档库中创建或上传 Excel 文件。文件可以是新建的,也可以使用已有文件。
|
|
106
|
+
|
|
107
|
+
建议表头按分析目标设计,最小表头:
|
|
108
|
+
|
|
109
|
+
| date | title | content |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
|
|
112
|
+
更推荐的通用表头:
|
|
113
|
+
|
|
114
|
+
| date | sender | title | content | messageId |
|
|
115
|
+
|---|---|---|---|---|
|
|
116
|
+
|
|
117
|
+
可选字段:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
bodyPreview
|
|
121
|
+
receivedTime
|
|
122
|
+
toRecipients
|
|
123
|
+
ccRecipients
|
|
124
|
+
importance
|
|
125
|
+
hasAttachments
|
|
126
|
+
conversationId
|
|
127
|
+
webLink
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 1.3 必须转换为 Excel Table
|
|
131
|
+
|
|
132
|
+
Power Automate 的 `在表中插入新行` 只能写入 Excel Table,不能写入普通单元格区域。
|
|
133
|
+
|
|
134
|
+
操作:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
选中表头和数据区域
|
|
138
|
+
→ 插入
|
|
139
|
+
→ 表格
|
|
140
|
+
→ 勾选“表包含标题”
|
|
141
|
+
→ 表格设计 / Table Design
|
|
142
|
+
→ 表名称 / Table Name
|
|
143
|
+
→ 设置一个稳定表名
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
表名建议:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
EmailCaptureTable
|
|
150
|
+
OutlookMailLog
|
|
151
|
+
KeywordMailTable
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
注意:
|
|
155
|
+
|
|
156
|
+
- 表名称不是 Sheet 名称。
|
|
157
|
+
- 表名称不是文件名。
|
|
158
|
+
- 如果 Power Automate 的表下拉显示 `无项`,通常就是文件里还没有 Excel Table。
|
|
159
|
+
- 如果刚创建表后 Power Automate 仍识别不到,保存并关闭 Excel,再重新选择文件和表。
|
|
160
|
+
|
|
161
|
+
## 2. Power Automate 创建 Flow
|
|
162
|
+
|
|
163
|
+
### 2.1 创建 Flow
|
|
164
|
+
|
|
165
|
+
在 Power Automate 中创建:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
创建
|
|
169
|
+
→ 自动化云端流
|
|
170
|
+
→ 选择触发器:Office 365 Outlook - 新电子邮件到达时(V3)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Flow 命名建议体现业务场景:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
Capture Outlook emails to SharePoint Excel
|
|
177
|
+
<业务名> email capture
|
|
178
|
+
<关键词> mail log
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 2.2 配置邮件触发器
|
|
182
|
+
|
|
183
|
+
节点:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
Office 365 Outlook
|
|
187
|
+
→ 新电子邮件到达时(V3)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
这个触发器一般高频用到 9 类参数。不同租户语言和版本名称会略有差异。
|
|
191
|
+
|
|
192
|
+
| 参数 | 用途 | 建议 |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| 文件夹 / Folder | 监听哪个邮箱文件夹 | 通常选 Inbox / 收件箱 |
|
|
195
|
+
| 至 / To | 收件人过滤 | 只有需要按收件人过滤时填写 |
|
|
196
|
+
| 抄送 / Cc | 抄送过滤 | 需要区分抄送邮件时填写 |
|
|
197
|
+
| 收件人或抄送 / To or Cc | To/Cc 任一命中 | 比分别填 To/Cc 更宽松 |
|
|
198
|
+
| 发件人 / From | 发件人过滤 | 适合固定发件人规则 |
|
|
199
|
+
| 主题筛选器 / Subject Filter | 主题关键词过滤 | 直接填关键词,不写 `contains()` |
|
|
200
|
+
| 加入附件 / Include Attachments | 是否把附件内容带出 | 默认否,除非后续要处理附件 |
|
|
201
|
+
| 只检索带附件的电子邮件 / Only with Attachments | 是否只要有附件邮件 | 默认否 |
|
|
202
|
+
| 重要性 / Importance | 按重要性过滤 | 很少用,除非业务明确需要 |
|
|
203
|
+
|
|
204
|
+
常见规则:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
发件人 = sender@example.com
|
|
208
|
+
主题筛选器 = KEYWORD
|
|
209
|
+
文件夹 = Inbox
|
|
210
|
+
加入附件 = 否
|
|
211
|
+
只检索带附件的电子邮件 = 否
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
规则放置原则:
|
|
215
|
+
|
|
216
|
+
- 简单发件人和主题规则优先放在触发器参数里,减少后续无效运行。
|
|
217
|
+
- 多条件、大小写、正文关键词、复杂收件人判断,放到后续 `Condition / 条件`。
|
|
218
|
+
- `Subject Filter` 填纯关键词,例如 `KEYWORD`,不要填表达式。
|
|
219
|
+
|
|
220
|
+
复杂条件示例:
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
and(
|
|
224
|
+
equals(toLower(triggerOutputs()?['body/from/emailAddress/address']), 'sender@example.com'),
|
|
225
|
+
contains(toLower(triggerOutputs()?['body/subject']), 'keyword')
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 2.3 理解新邮件触发器会带出的数据
|
|
230
|
+
|
|
231
|
+
`新电子邮件到达时(V3)` 触发后已经拿到邮件的动态内容。不要再额外添加“获取标题”或“获取正文”的动作,除非需要按 Message Id 获取更完整的对象。
|
|
232
|
+
|
|
233
|
+
常见动态字段大约有 34 类,具体数量会随连接器版本、租户语言、附件配置而变化。按类别理解即可:
|
|
234
|
+
|
|
235
|
+
| 类别 | 常见字段 |
|
|
236
|
+
|---|---|
|
|
237
|
+
| 标识 | Message Id, Internet Message Id, Conversation Id |
|
|
238
|
+
| 时间 | Received time, Sent time, Created date time, Last modified date time |
|
|
239
|
+
| 标题 | Subject |
|
|
240
|
+
| 正文 | Body, Body preview, Body content type |
|
|
241
|
+
| 发件人 | From, From name, From address, Sender |
|
|
242
|
+
| 收件人 | To, To recipients, Cc recipients, Bcc recipients |
|
|
243
|
+
| 状态 | Importance, Is read, Is HTML, Has attachments |
|
|
244
|
+
| 分类 | Categories, Flag |
|
|
245
|
+
| 链接 | Web link |
|
|
246
|
+
| 附件 | Attachments, Attachment name/content/content type/size |
|
|
247
|
+
| 元信息 | Parent folder id, Inference classification, Sensitivity |
|
|
248
|
+
|
|
249
|
+
优先映射字段:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
date = Received time
|
|
253
|
+
sender = From address
|
|
254
|
+
title = Subject
|
|
255
|
+
content = Body 或 Html 到文本输出
|
|
256
|
+
messageId = Message Id
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 2.4 Html 到文本
|
|
260
|
+
|
|
261
|
+
Outlook 正文通常是 HTML。写入 Excel 前建议加:
|
|
262
|
+
|
|
263
|
+
```text
|
|
264
|
+
Content Conversion
|
|
265
|
+
→ Html 到文本 / Html to text
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
配置:
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
内容 / Content = 新电子邮件到达时(V3) → Body / 正文
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
用途:
|
|
275
|
+
|
|
276
|
+
- 去掉 `<div>`、`<br>`、`span` 等 HTML 标签。
|
|
277
|
+
- 让 Excel 中的 `content` 更适合人工查看和脚本分析。
|
|
278
|
+
- 避免后续本地分析时先做 HTML 清洗。
|
|
279
|
+
|
|
280
|
+
如果用户确实需要保留原始 HTML,可同时写两列:
|
|
281
|
+
|
|
282
|
+
```text
|
|
283
|
+
contentText = Html 到文本输出
|
|
284
|
+
contentHtml = Body
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 2.5 Excel Online (Business)
|
|
288
|
+
|
|
289
|
+
连接器:
|
|
290
|
+
|
|
291
|
+
```text
|
|
292
|
+
Excel Online (Business)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
当前方法使用的动作:
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
在表中插入新行 / Add a row into a table
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
配置顺序:
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
位置 / Location:选择 SharePoint Site - <site name>
|
|
305
|
+
文档库 / Document Library:选择文档库,例如 文档 / Documents
|
|
306
|
+
文件 / File:用文件夹图标浏览选择 Excel 文件
|
|
307
|
+
表 / Table:选择 Excel Table 名称
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
不要优先手填文件路径。浏览选择文件更稳定,能减少 schema 识别异常。
|
|
311
|
+
|
|
312
|
+
选择表后,Excel Table 的表头会展开为可填写字段。映射示例:
|
|
313
|
+
|
|
314
|
+
```text
|
|
315
|
+
date = Received time
|
|
316
|
+
sender = From address
|
|
317
|
+
title = Subject
|
|
318
|
+
content = Html 到文本 → The plain text content
|
|
319
|
+
messageId = Message Id
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
如果只设计了三列表头:
|
|
323
|
+
|
|
324
|
+
```text
|
|
325
|
+
date = Received time
|
|
326
|
+
title = Subject
|
|
327
|
+
content = Html 到文本输出
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 3. 获取 SharePoint 中的数据
|
|
331
|
+
|
|
332
|
+
根据用户后续分析方式选择获取路径。
|
|
333
|
+
|
|
334
|
+
### 3.1 推荐路径:SharePoint Sync 到 OneDrive 本地
|
|
335
|
+
|
|
336
|
+
优先让用户在 SharePoint 文档库中使用 `Sync / 同步`,把包含 Excel 的文档库同步到 OneDrive 本地目录。后续分析直接读本地同步出来的 `.xlsx` 文件,不再依赖 Graph API 或浏览器下载。
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
SharePoint Excel
|
|
340
|
+
→ SharePoint 文档库点击 Sync / 同步
|
|
341
|
+
→ OneDrive 在本机创建同步目录
|
|
342
|
+
→ Power Automate 写入 SharePoint Excel
|
|
343
|
+
→ OneDrive 在一段时间后同步更新到本地
|
|
344
|
+
→ 本地脚本读取同步路径下的 Excel 文件
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
落地时记录:
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
SharePoint 文件 URL
|
|
351
|
+
OneDrive 本地同步路径
|
|
352
|
+
Excel 文件名
|
|
353
|
+
Excel Table 名称
|
|
354
|
+
表头字段
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
注意:
|
|
358
|
+
|
|
359
|
+
- OneDrive 同步不是实时强一致,Power Automate 更新后本地文件会有延迟。
|
|
360
|
+
- 分析脚本应把同步文件当作只读输入,避免和 OneDrive/Excel/Power Automate 同时写同一个文件。
|
|
361
|
+
- 如果要稳定分析,建议脚本读取 `.xlsx` 后转存为本地 `CSV / JSONL / SQLite`,后续分析读转存结果。
|
|
362
|
+
- 如果本地脚本读到旧数据,先检查 OneDrive 同步状态,而不是立即怀疑 Flow 写入失败。
|
|
363
|
+
|
|
364
|
+
这个路径适合个人分析和低频数据,是企业租户限制较多时最稳的默认方案。
|
|
365
|
+
|
|
366
|
+
### 3.2 手动下载 Excel
|
|
367
|
+
|
|
368
|
+
适合临时分析:
|
|
369
|
+
|
|
370
|
+
```text
|
|
371
|
+
SharePoint 文档库
|
|
372
|
+
→ 下载 Excel
|
|
373
|
+
→ 本地读取
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 3.3 脚本读取同步后的 Excel
|
|
377
|
+
|
|
378
|
+
如果文件已经同步到本地,可以用脚本读取 Excel Table 或工作表数据,输出 CSV/JSONL/SQLite。
|
|
379
|
+
|
|
380
|
+
注意:
|
|
381
|
+
|
|
382
|
+
- 分析脚本不要在 Excel 桌面版打开文件时写入同一文件。
|
|
383
|
+
- 如果 Flow 正在写入,脚本侧优先只读。
|
|
384
|
+
- 更稳定的长期分析格式是从 Excel 转存到 CSV/SQLite。
|
|
385
|
+
|
|
386
|
+
### 3.4 Graph API 路径:默认标记为需授权
|
|
387
|
+
|
|
388
|
+
如果企业授权允许,可用 Microsoft Graph 或 SharePoint API 读取文件内容,但不要默认假设可用。企业租户通常需要管理员同意权限、条件访问放行、已批准的 App Registration 或已批准的第一方客户端。
|
|
389
|
+
|
|
390
|
+
已验证的失败信号:
|
|
391
|
+
|
|
392
|
+
```text
|
|
393
|
+
AADSTS65002
|
|
394
|
+
Consent between first party application ... and Microsoft Graph must be configured via preauthorization
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
含义:当前公共客户端不能在该租户中直接申请 Graph 权限。
|
|
398
|
+
|
|
399
|
+
```text
|
|
400
|
+
Your sign-in was successful but does not meet the criteria to access this resource
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
含义:登录成功,但被条件访问、应用访问策略、设备/位置/认证流规则拦截。
|
|
404
|
+
|
|
405
|
+
遇到这些错误时,认为临时 Graph 读取路径不可用:
|
|
406
|
+
|
|
407
|
+
```text
|
|
408
|
+
不能读取 workbook/tables/usedRange
|
|
409
|
+
不能下载 /content
|
|
410
|
+
不能靠换 endpoint 绕过
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
只有在用户提供公司已批准的 Graph 应用、管理员已 consent 的权限、或确认 Graph Explorer/Graph PowerShell 可用时,才继续尝试 Graph。
|
|
414
|
+
|
|
415
|
+
## 常见问题
|
|
416
|
+
|
|
417
|
+
### 表下拉显示“无项”
|
|
418
|
+
|
|
419
|
+
原因通常是 Excel 文件没有真正的 Excel Table。
|
|
420
|
+
|
|
421
|
+
处理:
|
|
422
|
+
|
|
423
|
+
```text
|
|
424
|
+
1. 打开 Excel Online
|
|
425
|
+
2. 选中表头和数据区域
|
|
426
|
+
3. 插入 → 表格
|
|
427
|
+
4. 保存并关闭文件
|
|
428
|
+
5. 回到 Power Automate 重新选择文件和表
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 选择表后没有出现列字段
|
|
432
|
+
|
|
433
|
+
处理顺序:
|
|
434
|
+
|
|
435
|
+
```text
|
|
436
|
+
1. 点击 高级参数 → 全部显示
|
|
437
|
+
2. 确认表头没有空列、重复列、特殊符号
|
|
438
|
+
3. 在表格下方临时加一行 test 数据
|
|
439
|
+
4. 保存并关闭 Excel
|
|
440
|
+
5. 删除并重建 “在表中插入新行” 动作
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### 报错 “Item/列1 在操作架构中不再存在”
|
|
444
|
+
|
|
445
|
+
Power Automate 缓存了旧的 Excel schema。
|
|
446
|
+
|
|
447
|
+
最快修复:
|
|
448
|
+
|
|
449
|
+
```text
|
|
450
|
+
1. 删除当前 Excel 写入动作
|
|
451
|
+
2. 保存 Flow
|
|
452
|
+
3. 退出并重新进入 Flow 编辑器
|
|
453
|
+
4. 重新添加 Excel Online (Business) → 在表中插入新行
|
|
454
|
+
5. 重新选择位置、文档库、文件、表
|
|
455
|
+
6. 重新映射字段
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### 写入后正文有 HTML 标签
|
|
459
|
+
|
|
460
|
+
在 Excel 写入前增加:
|
|
461
|
+
|
|
462
|
+
```text
|
|
463
|
+
Html 到文本
|
|
464
|
+
Content = Body
|
|
465
|
+
content 字段 = Html 到文本输出
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Excel 文件被锁
|
|
469
|
+
|
|
470
|
+
避免用桌面版 Excel 长时间打开目标文件。多人同时编辑或桌面版占用时,Flow 写入可能失败。
|
|
471
|
+
|
|
472
|
+
### 文件路径手填后识别异常
|
|
473
|
+
|
|
474
|
+
优先用文件夹图标选择文件。如果必须手填路径,确认路径是文档库内路径,而不是分享链接。
|
|
475
|
+
|
|
476
|
+
## 验证流程
|
|
477
|
+
|
|
478
|
+
保存 Flow 后:
|
|
479
|
+
|
|
480
|
+
```text
|
|
481
|
+
测试
|
|
482
|
+
→ 手动测试
|
|
483
|
+
→ 发送一封满足筛选条件的邮件
|
|
484
|
+
→ 查看运行历史记录
|
|
485
|
+
→ 打开 Excel 检查是否新增一行
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
运行历史排查顺序:
|
|
489
|
+
|
|
490
|
+
```text
|
|
491
|
+
1. 新电子邮件到达时(V3):是否触发、字段是否正确
|
|
492
|
+
2. Html 到文本:正文是否转换成功
|
|
493
|
+
3. 在表中插入新行:位置、文件、表、列映射是否正确
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
如果触发器没跑,优先检查发件人、主题筛选器、文件夹和邮件是否真的进入目标邮箱。
|
package/src/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { packageRoot } from './utils/paths.js';
|
|
|
5
5
|
const HELP = `Usage: ae <command> [options]
|
|
6
6
|
|
|
7
7
|
Commands:
|
|
8
|
-
setup Install skills to .
|
|
8
|
+
setup Install skills to selected AI coding platforms.
|
|
9
9
|
init [goal] Create spec/ skeleton and render entry files.
|
|
10
10
|
Requires \`ae setup\` to have run.
|
|
11
11
|
|
package/src/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { createDirectories, renderEntryFiles } from '../skeleton.js';
|
|
4
4
|
import { templatesPath } from '../utils/paths.js';
|
|
5
5
|
import { formatInitReport } from '../utils/report.js';
|
|
6
|
+
import { PLATFORMS, getPlatformSkillsDir } from '../platforms.js';
|
|
6
7
|
|
|
7
8
|
const DEFAULT_GOAL = 'TODO: 用一句话说明这个项目要解决什么问题。';
|
|
8
9
|
|
|
@@ -111,10 +112,12 @@ async function ensureTargetDirectory(target, dryRun) {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
async function hasInstalledAssets(target) {
|
|
114
|
-
|
|
115
|
-
(await isDirectory(path.join(target,
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
for (const platform of PLATFORMS) {
|
|
116
|
+
if (await isDirectory(path.join(target, getPlatformSkillsDir(platform)))) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
async function statMaybe(value) {
|
package/src/commands/setup.js
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { mkdir, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
4
|
import { copyTreeIdempotent } from '../utils/copy.js';
|
|
4
5
|
import { skillsPath } from '../utils/paths.js';
|
|
5
6
|
import { formatSetupReport } from '../utils/report.js';
|
|
7
|
+
import {
|
|
8
|
+
PLATFORMS,
|
|
9
|
+
detectPlatforms,
|
|
10
|
+
getPlatformSkillsDir,
|
|
11
|
+
listPlatformIds,
|
|
12
|
+
parsePlatformList,
|
|
13
|
+
resolvePlatforms,
|
|
14
|
+
} from '../platforms.js';
|
|
6
15
|
|
|
7
16
|
const HELP = `Usage: ae setup [options]
|
|
8
17
|
|
|
9
18
|
Options:
|
|
10
19
|
--target <path> Install AE assets into this project directory. Defaults to cwd.
|
|
20
|
+
--platform <ids> Install for comma-separated platforms, e.g. claude,codex,cursor.
|
|
11
21
|
--dry-run Show what would be installed without writing files.
|
|
12
|
-
-h, --help Show help
|
|
22
|
+
-h, --help Show help
|
|
23
|
+
|
|
24
|
+
Platforms:
|
|
25
|
+
${listPlatformIds().join(', ')}`;
|
|
13
26
|
|
|
14
27
|
export async function run(argv = []) {
|
|
15
28
|
const options = parseArgs(argv);
|
|
@@ -22,20 +35,20 @@ export async function run(argv = []) {
|
|
|
22
35
|
const target = path.resolve(options.target ?? process.cwd());
|
|
23
36
|
|
|
24
37
|
try {
|
|
38
|
+
const explicitPlatforms =
|
|
39
|
+
options.platformIds.length > 0 ? resolvePlatforms(options.platformIds) : undefined;
|
|
25
40
|
await ensureTargetDirectory(target, options.dryRun);
|
|
41
|
+
const selectedPlatforms = explicitPlatforms ?? (await selectPlatforms(target));
|
|
26
42
|
|
|
27
43
|
const created = [];
|
|
28
44
|
const skipped = [];
|
|
29
|
-
const skillGroups =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
prefix: path.join('.codex', 'skills'),
|
|
37
|
-
},
|
|
38
|
-
];
|
|
45
|
+
const skillGroups = selectedPlatforms.map((platform) => {
|
|
46
|
+
const skillsDir = getPlatformSkillsDir(platform);
|
|
47
|
+
return {
|
|
48
|
+
dest: path.join(target, skillsDir),
|
|
49
|
+
prefix: skillsDir,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
39
52
|
|
|
40
53
|
for (const group of skillGroups) {
|
|
41
54
|
const result = await copyTreeIdempotent(skillsPath(), group.dest, {
|
|
@@ -57,6 +70,7 @@ export async function run(argv = []) {
|
|
|
57
70
|
function parseArgs(argv) {
|
|
58
71
|
const options = {
|
|
59
72
|
target: undefined,
|
|
73
|
+
platformIds: [],
|
|
60
74
|
dryRun: false,
|
|
61
75
|
help: false,
|
|
62
76
|
};
|
|
@@ -67,6 +81,13 @@ function parseArgs(argv) {
|
|
|
67
81
|
options.help = true;
|
|
68
82
|
} else if (arg === '--dry-run') {
|
|
69
83
|
options.dryRun = true;
|
|
84
|
+
} else if (arg === '--platform') {
|
|
85
|
+
const value = argv[index + 1];
|
|
86
|
+
if (!value) {
|
|
87
|
+
throw new Error('--platform requires a comma-separated platform list');
|
|
88
|
+
}
|
|
89
|
+
options.platformIds.push(...parsePlatformList(value));
|
|
90
|
+
index += 1;
|
|
70
91
|
} else if (arg === '--target') {
|
|
71
92
|
const value = argv[index + 1];
|
|
72
93
|
if (!value) {
|
|
@@ -82,6 +103,44 @@ function parseArgs(argv) {
|
|
|
82
103
|
return options;
|
|
83
104
|
}
|
|
84
105
|
|
|
106
|
+
async function selectPlatforms(target) {
|
|
107
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`No platforms selected. In non-interactive mode, pass --platform <ids>. Available: ${listPlatformIds().join(', ')}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const detected = await detectPlatforms(target);
|
|
114
|
+
const detectedIds = detected.map((platform) => platform.id);
|
|
115
|
+
const defaultIds = detectedIds;
|
|
116
|
+
|
|
117
|
+
console.log('Select target platforms for AE skills:');
|
|
118
|
+
for (const [index, platform] of PLATFORMS.entries()) {
|
|
119
|
+
const marker = defaultIds.includes(platform.id) ? '*' : ' ';
|
|
120
|
+
console.log(` ${index + 1}. [${marker}] ${platform.name} (${platform.id}) -> ${getPlatformSkillsDir(platform)}/`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const defaultPrompt = defaultIds.length > 0 ? ` [${defaultIds.join(',')}]` : '';
|
|
124
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
+
try {
|
|
126
|
+
const answer = await rl.question(`Platform ids or numbers, comma-separated${defaultPrompt}: `);
|
|
127
|
+
const selected = answer.trim() ? parseInteractivePlatformAnswer(answer) : defaultIds;
|
|
128
|
+
return resolvePlatforms(selected);
|
|
129
|
+
} finally {
|
|
130
|
+
rl.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseInteractivePlatformAnswer(answer) {
|
|
135
|
+
return parsePlatformList(answer).map((item) => {
|
|
136
|
+
const number = Number(item);
|
|
137
|
+
if (Number.isInteger(number) && number >= 1 && number <= PLATFORMS.length) {
|
|
138
|
+
return PLATFORMS[number - 1].id;
|
|
139
|
+
}
|
|
140
|
+
return item;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
85
144
|
async function ensureTargetDirectory(target, dryRun) {
|
|
86
145
|
try {
|
|
87
146
|
const current = await stat(target);
|
package/src/platforms.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const PLATFORMS = [
|
|
5
|
+
{ id: 'claude', name: 'Claude Code', skillsDir: '.claude', aliases: ['claude-code'] },
|
|
6
|
+
{ id: 'codex', name: 'Codex', skillsDir: '.codex' },
|
|
7
|
+
{ id: 'windsurf', name: 'Windsurf', skillsDir: '.windsurf' },
|
|
8
|
+
{
|
|
9
|
+
id: 'github-copilot',
|
|
10
|
+
name: 'GitHub Copilot',
|
|
11
|
+
skillsDir: '.github',
|
|
12
|
+
aliases: ['copilot', 'github'],
|
|
13
|
+
detectionPaths: [
|
|
14
|
+
'.github/copilot-instructions.md',
|
|
15
|
+
'.github/instructions',
|
|
16
|
+
'.github/prompts',
|
|
17
|
+
'.github/skills',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{ id: 'kilocode', name: 'Kilo Code', skillsDir: '.kilocode', aliases: ['kilo-code'] },
|
|
21
|
+
{ id: 'kimicode', name: 'Kimi Code', skillsDir: '.kimi-code', aliases: ['kimi', 'kimi-code'] },
|
|
22
|
+
{ id: 'codebuddy', name: 'CodeBuddy', skillsDir: '.codebuddy', aliases: ['codebuddy-code'] },
|
|
23
|
+
{ id: 'qoder', name: 'Qoder', skillsDir: '.qoder' },
|
|
24
|
+
{ id: 'trae', name: 'Trae', skillsDir: '.trae' },
|
|
25
|
+
{ id: 'cursor', name: 'Cursor', skillsDir: '.cursor' },
|
|
26
|
+
{ id: 'opencode', name: 'OpenCode', skillsDir: '.opencode', aliases: ['open-code'] },
|
|
27
|
+
{ id: 'antigravity', name: 'Antigravity', skillsDir: '.agents' },
|
|
28
|
+
{ id: 'gemini', name: 'Gemini CLI', skillsDir: '.gemini', aliases: ['gemini-cli'] },
|
|
29
|
+
{ id: 'qwen', name: 'Qwen Code', skillsDir: '.qwen', aliases: ['qwen-code'] },
|
|
30
|
+
{ id: 'kiro', name: 'Kiro', skillsDir: '.kiro' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const PLATFORM_BY_ID_OR_ALIAS = new Map(
|
|
34
|
+
PLATFORMS.flatMap((platform) => [
|
|
35
|
+
[platform.id, platform],
|
|
36
|
+
...(platform.aliases ?? []).map((alias) => [alias, platform]),
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export function listPlatformIds() {
|
|
41
|
+
return PLATFORMS.map((platform) => platform.id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolvePlatforms(ids) {
|
|
45
|
+
const resolved = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const unknown = [];
|
|
48
|
+
|
|
49
|
+
for (const rawId of ids) {
|
|
50
|
+
const id = rawId.trim().toLowerCase();
|
|
51
|
+
if (!id) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const platform = PLATFORM_BY_ID_OR_ALIAS.get(id);
|
|
56
|
+
if (!platform) {
|
|
57
|
+
unknown.push(rawId);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!seen.has(platform.id)) {
|
|
62
|
+
resolved.push(platform);
|
|
63
|
+
seen.add(platform.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (unknown.length > 0) {
|
|
68
|
+
throw new Error(`Unknown platform(s): ${unknown.join(', ')}. Available: ${listPlatformIds().join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (resolved.length === 0) {
|
|
72
|
+
throw new Error(`No platforms selected. Available: ${listPlatformIds().join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parsePlatformList(value) {
|
|
79
|
+
return value
|
|
80
|
+
.split(',')
|
|
81
|
+
.map((item) => item.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getPlatformSkillsDir(platform) {
|
|
86
|
+
return path.join(platform.skillsDir, 'skills');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function detectPlatforms(target) {
|
|
90
|
+
const detected = [];
|
|
91
|
+
|
|
92
|
+
for (const platform of PLATFORMS) {
|
|
93
|
+
if (platform.detectionPaths?.length > 0) {
|
|
94
|
+
for (const detectionPath of platform.detectionPaths) {
|
|
95
|
+
if (await pathExists(path.join(target, detectionPath))) {
|
|
96
|
+
detected.push(platform);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (await isDirectory(path.join(target, platform.skillsDir))) {
|
|
104
|
+
detected.push(platform);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return detected;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function pathExists(value) {
|
|
112
|
+
try {
|
|
113
|
+
await stat(value);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.code === 'ENOENT') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function isDirectory(value) {
|
|
124
|
+
try {
|
|
125
|
+
return (await stat(value)).isDirectory();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.code === 'ENOENT') {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|