forge-connector
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseForge Connector
Forge连接器
Builds a Forge app that ingests external data into Atlassian's Teamwork Graph so it appears in Rovo Search and Rovo Chat.
graph:connector构建一个类型的Forge应用,将外部数据导入Atlassian的Teamwork Graph,使其在Rovo Search和Rovo Chat中展示。
graph:connectorCritical Rules
核心规则
- Must install in Jira — Apps using Teamwork Graph modules must be installed on a Jira site. Confluence-only installs will not work.
- Never ask for credentials in chat — Direct users to run in their own terminal.
forge login - Always run the scaffold script yourself — Do not only give manual instructions; run to generate the boilerplate.
scripts/scaffold_connector.py - Always ask the user for their Atlassian site URL when install is needed — never discover or guess it.
- Atlassian deletes data on disconnect — When , the app only needs to clean up local state; Atlassian removes the Teamwork Graph data automatically.
action = 'DELETED' - Handler arguments are passed directly — Forge passes the request object as the first argument to handlers, NOT nested under . Config values are at
event.payload, NOTrequest.configProperties. This is the most common source ofevent.payload.configerrors.TypeError: Cannot destructure property of undefined - Use for storage — Import
@forge/kvsfromkvs. Do NOT use@forge/kvs— its@forge/storageexport isstorageat runtime in connector functions.undefined - Use named export from
graph— The correct import is@forge/teamwork-graph. Callconst { graph } = require('@forge/teamwork-graph'). Do NOT importgraph.setObjects({ objects, connectionId })as a named export directly.setObjects - must return
validateConnectionHandler— Do NOT throw an Error. Return{ success, message }to reject,{ success: false, message: '...' }to accept.{ success: true } - declarations belong under
function— Inmodules,manifest.ymlis a key underfunction:, not a top-level key. Placing it at the top level causes a lint error.modules: - uses
formConfigurationarray withform— Do NOT usetype: headerorfields:. The correct format usesbeforeYouBegin:.form: [{ key, type: header, title, description, properties: [...] }] - Scopes are — Use
read/write/delete:object:jira,read:object:jira,write:object:jira. The scopesdelete:object:jiraandread:graph:teamworkare invalid and will failwrite:graph:teamwork.forge lint
- 必须安装在Jira中 — 使用Teamwork Graph模块的应用必须安装在Jira站点上。仅安装在Confluence中将无法正常工作。
- 切勿在聊天中索要凭据 — 引导用户在自己的终端中运行。
forge login - 务必自行运行脚手架脚本 — 不要仅提供手动操作说明;运行来生成样板代码。
scripts/scaffold_connector.py - 当需要安装时,务必询问用户的Atlassian站点URL — 切勿自行探测或猜测。
- Atlassian会在断开连接时删除数据 — 当时,应用只需清理本地状态;Atlassian会自动移除Teamwork Graph中的数据。
action = 'DELETED' - 处理器参数直接传递 — Forge将请求对象作为第一个参数传递给处理器,而非嵌套在下。配置值位于
event.payload,而非request.configProperties。这是event.payload.config错误最常见的原因。TypeError: Cannot destructure property of undefined - 使用进行存储 — 从
@forge/kvs导入@forge/kvs。请勿使用kvs— 其@forge/storage导出在连接器函数的运行时为storage。undefined - 使用中的
@forge/teamwork-graph命名导出 — 正确的导入方式是graph。调用const { graph } = require('@forge/teamwork-graph')。请勿直接将graph.setObjects({ objects, connectionId })作为命名导出导入。setObjects - 必须返回
validateConnectionHandler— 切勿抛出Error。返回{ success, message }表示拒绝,返回{ success: false, message: '...' }表示接受。{ success: true } - 声明需放在
function下 — 在modules中,manifest.yml是function:下的一个键,而非顶层键。将其放在顶层会导致lint错误。modules: - 使用包含
formConfiguration的type: header数组 — 请勿使用form或fields:。正确格式为beforeYouBegin:。form: [{ key, type: header, title, description, properties: [...] }] - 权限范围为— 使用
read/write/delete:object:jira、read:object:jira、write:object:jira。权限范围delete:object:jira和read:graph:teamwork无效,会导致write:graph:teamwork失败。forge lint
MCP Prerequisites
MCP前置条件
| MCP Server | Purpose |
|---|---|
| Forge MCP | Manifest syntax, module config, deployment guides |
| ADS MCP | Atlaskit components (only if adding Custom UI) |
| MCP服务器 | 用途 |
|---|---|
| Forge MCP | 清单语法、模块配置、部署指南 |
| ADS MCP | Atlaskit组件(仅在添加自定义UI时使用) |
Agent Workflow — Complete Steps 0–5 in Order
Agent工作流 — 按顺序完成步骤0–5
Step 0: Prerequisites
步骤0:前置检查
Check Node.js (, requires 22+), Forge CLI (), and login (). Install missing tools:
node -vforge --versionforge whoamibash
npm install -g @forge/cliTell the user to run in their terminal if not authenticated.
forge login检查Node.js(,要求22+)、Forge CLI()和登录状态()。安装缺失的工具:
node -vforge --versionforge whoamibash
npm install -g @forge/cli如果用户未认证,告知其在终端中运行。
forge loginStep 1: Discover Developer Spaces
步骤1:发现开发者空间
Note:does NOT exist in Forge CLI 12.x. You cannot list developer spaces non-interactively.forge developer-spaces list
forge createTell the user:
cd <parent-directory>
forge create --template blank <app-name>
When prompted, select a Developer Space and let it complete.
Come back when done.The flag in the scaffold script is optional and can be omitted — the script has been updated to skip it when not provided.
--dev-space-id注意: Forge CLI 12.x中不存在命令。无法以非交互方式列出开发者空间。forge developer-spaces list
forge create告知用户:
cd <父目录>
forge create --template blank <应用名称>
当出现提示时,选择一个开发者空间并等待完成。
完成后返回。脚手架脚本中的标记是可选的,可以省略 — 脚本已更新为在未提供该标记时跳过相关步骤。
--dev-space-idStep 2: Scaffold the Connector App
步骤2:搭建连接器应用脚手架
Run from the skill directory (the directory containing this SKILL.md). is optional:
--dev-space-idbash
python3 -m scripts.scaffold_connector \
--name <app-name> \
--connector-name "<Human Readable Name>" \
--object-type atlassian:document \
--directory <parent-directory>Add only if you have the ID from a previous step.
--dev-space-id <id>Object type selection — pick the type that best matches the content being ingested (see Object Types table). For mixed content, use as the default.
atlassian:documentForm config flag — add if the admin must provide API credentials or connection details (typical for external systems). Omit it for apps that operate entirely within Atlassian (no external credentials needed).
--has-form-configIf scaffold fails becauseneeds a TTY: The scaffold script will print a manual fallback command. Have the user runforge createinteractively, then continue from Step 3 — the scaffold script only needs to writeforge createandmanifest.ymlafter the directory exists.src/index.js
从技能目录(包含本SKILL.md的目录)运行。为可选参数:
--dev-space-idbash
python3 -m scripts.scaffold_connector \
--name <应用名称> \
--connector-name "<易读名称>" \
--object-type atlassian:document \
--directory <父目录>仅当你有上一步获取的ID时,才添加。
--dev-space-id <id>对象类型选择 — 选择与要导入的内容最匹配的类型(参见对象类型表)。对于混合内容,默认使用。
atlassian:document表单配置标记 — 如果管理员必须提供API凭据或连接详情(外部系统的典型场景),则添加。对于完全在Atlassian内部运行的应用(无需外部凭据),可省略该标记。
--has-form-config如果脚手架因需要TTY而失败: 脚手架脚本会打印手动回退命令。让用户交互式运行forge create,然后从步骤3继续 — 脚手架脚本只需在目录创建后写入forge create和manifest.yml。src/index.js
Step 3: Customize the Generated Code
步骤3:自定义生成的代码
After scaffolding (or after the user runs interactively):
forge createbash
cd <app-name>
npm installThe blank template generates (JavaScript, not TypeScript). Edit it to add your API calls. The scaffold generates working handler skeletons; fill in your business logic.
src/index.js完成脚手架搭建(或用户交互式运行)后:
forge createbash
cd <应用名称>
npm install空白模板会生成(JavaScript,而非TypeScript)。编辑该文件以添加API调用。脚手架会生成可用的处理器骨架;请填充你的业务逻辑。
src/index.jsKey files to edit
需要编辑的关键文件
| File | What to change |
|---|---|
| |
| Add |
| Add |
| 文件 | 修改内容 |
|---|---|
| |
| 为所有外部API添加 |
| 添加 |
setObjects — ingest data into Teamwork Graph
setObjects — 将数据导入Teamwork Graph
Use the named export — do NOT destructure directly:
graphsetObjectsjavascript
const { graph } = require('@forge/teamwork-graph');
const result = await graph.setObjects({
connectionId, // required — the connectionId from the handler request
objects: [
{
schemaVersion: '1.0',
id: 'unique-id-from-source', // unique per connectionId
updateSequenceNumber: 1,
displayName: 'My Document Title',
url: 'https://source-system.example.com/doc/123',
createdAt: '2024-01-15T10:00:00Z', // ISO 8601
lastUpdatedAt: '2024-01-20T14:30:00Z',
permissions: [{
accessControls: [{
principals: [{ type: 'EVERYONE' }], // or restrict to specific users
}],
}],
'atlassian:document': {
type: {
category: 'DOCUMENT', // see Document Categories table below
mimeType: 'application/vnd.google-apps.document',
},
content: {
mimeType: 'application/vnd.google-apps.document',
text: 'document title or snippet for search indexing',
},
},
},
],
});
if (!result.success) {
console.error('setObjects error:', result.error);
}- Max 100 objects per call — batch large datasets with a loop
- must be unique per
idconnectionId - is required in every
connectionIdcallgraph.setObjects()
使用命名导出 — 请勿直接解构:
graphsetObjectsjavascript
const { graph } = require('@forge/teamwork-graph');
const result = await graph.setObjects({
connectionId, // 必填 — 处理器请求中的connectionId
objects: [
{
schemaVersion: '1.0',
id: '来自数据源的唯一ID', // 每个connectionId下唯一
updateSequenceNumber: 1,
displayName: '我的文档标题',
url: 'https://source-system.example.com/doc/123',
createdAt: '2024-01-15T10:00:00Z', // ISO 8601格式
lastUpdatedAt: '2024-01-20T14:30:00Z',
permissions: [{
accessControls: [{
principals: [{ type: 'EVERYONE' }], // 或限制为特定用户
}],
}],
'atlassian:document': {
type: {
category: 'DOCUMENT', // 参见下方文档类别表
mimeType: 'application/vnd.google-apps.document',
},
content: {
mimeType: 'application/vnd.google-apps.document',
text: '用于搜索索引的文档标题或片段',
},
},
},
],
});
if (!result.success) {
console.error('setObjects错误:', result.error);
}- 每次调用最多100个对象 — 对于大型数据集,使用循环分批处理
- 在每个
id下必须唯一connectionId - 每次调用都必须传入
graph.setObjects()connectionId
Document Categories (for atlassian:document.type.category
)
atlassian:document.type.category文档类别(用于atlassian:document.type.category
)
atlassian:document.type.category| MIME type | Category |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| Other | |
| MIME类型 | 类别 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| 其他 | |
getObjectByExternalId — look up a single object
getObjectByExternalId — 查找单个对象
javascript
const { graph } = require('@forge/teamwork-graph');
const data = await graph.getObjectByExternalId({
externalId: 'unique-id-from-source',
objectType: 'atlassian:document',
connectionId,
});
if (data.success) console.log(data.object);javascript
const { graph } = require('@forge/teamwork-graph');
const data = await graph.getObjectByExternalId({
externalId: '来自数据源的唯一ID',
objectType: 'atlassian:document',
connectionId,
});
if (data.success) console.log(data.object);Step 4: Deploy and Install
步骤4:部署与安装
You MUST run the deploy script — do not only give the user manual commands.
forge deployThe deploy script lives in the forge-app-builder skill, not in this skill. Derive its directory from the path of this SKILL.md: go up two levels ( → ) then into . Run all commands below from that directory.
skills/forge-connector/skills/forge-app-builder/bash
undefined你必须运行部署脚本 — 不要仅向用户提供手动命令。
forge deploy部署脚本位于forge-app-builder技能中,而非本技能。从本SKILL.md的路径推导其目录:向上两级( → )然后进入。从该目录运行以下所有命令。
skills/forge-connector/skills/forge-app-builder/bash
undefinedDerive forge-app-builder skill dir from this SKILL.md's path:
从本SKILL.md的路径推导forge-app-builder技能目录:
e.g. if this file is at /path/to/skills/forge-connector/SKILL.md
例如,如果本文件位于/path/to/skills/forge-connector/SKILL.md
then the deploy script dir is: /path/to/skills/forge-app-builder/
则部署脚本目录为:/path/to/skills/forge-app-builder/
If you have the site URL:
如果已有站点URL:
python3 -m scripts.deploy_forge_app
--app-dir <app-directory>
--site <site-url>
--product jira
--app-dir <app-directory>
--site <site-url>
--product jira
python3 -m scripts.deploy_forge_app
--app-dir <应用目录>
--site <站点URL>
--product jira
--app-dir <应用目录>
--site <站点URL>
--product jira
If you don't have the site URL yet, deploy first then ask:
如果还没有站点URL,先部署再询问:
python3 -m scripts.deploy_forge_app
--app-dir <app-directory>
--product jira
--deploy-only
--app-dir <app-directory>
--product jira
--deploy-only
python3 -m scripts.deploy_forge_app
--app-dir <应用目录>
--product jira
--deploy-only
--app-dir <应用目录>
--product jira
--deploy-only
Ask: "What is your Atlassian site URL (e.g. yourcompany.atlassian.net)?"
询问:"你的Atlassian站点URL是什么(例如yourcompany.atlassian.net)?"
python3 -m scripts.deploy_forge_app
--app-dir <app-directory>
--site <site-url>
--product jira
--skip-deps
--app-dir <app-directory>
--site <site-url>
--product jira
--skip-deps
undefinedpython3 -m scripts.deploy_forge_app
--app-dir <应用目录>
--site <站点URL>
--product jira
--skip-deps
--app-dir <应用目录>
--site <站点URL>
--product jira
--skip-deps
undefinedStep 5: Connect via Atlassian Administration
步骤5:通过Atlassian管理后台连接
After deployment, tell the user to:
- Go to Atlassian Administration → Apps → [site] → Connected apps
- Find the app → View app details → Connections tab
- Click Connect under the connector
- Fill in any configuration fields (if was defined)
formConfiguration - Click Connect — this triggers with
onConnectionChangeand starts data ingestionaction: CREATED
部署完成后,告知用户:
- 进入Atlassian管理后台 → 应用 → [站点] → 已连接的应用
- 找到该应用 → 查看应用详情 → 连接标签页
- 点击连接器下方的连接
- 填写任何配置字段(如果定义了)
formConfiguration - 点击连接 — 这会触发并传入
onConnectionChange,同时开始数据导入action: CREATED
Step 6: Monitor with forge tunnel
步骤6:使用forge tunnel监控
Use during development to stream live logs directly to your terminal as the connector functions execute. This is the fastest way to catch errors in , , and calls without waiting for .
forge tunnelonConnectionChangeHandlervalidateConnectionHandlersetObjectsforge logsTell the user to run this in their own terminal (it requires an interactive session):
bash
cd <app-directory>
forge tunnelWith the tunnel active, any invocation of the connector functions (e.g. clicking "Connect" in Atlassian Admin, or triggering a scheduled re-ingestion) will stream output immediately. Look for:
- — confirms
[connector] Fetched N itemsranfetchExternalData() - — confirms
[connector] Batch 1: N accepted, 0 rejectedsucceededsetObjects - Any uncaught errors or thrown exceptions from
validateConnectionHandler
If the tunnel is not running, use instead to inspect past invocations:
forge logsbash
undefined在开发过程中使用,将连接器函数执行时的实时日志直接流式传输到你的终端。这是捕获、和调用错误最快的方式,无需等待。
forge tunnelonConnectionChangeHandlervalidateConnectionHandlersetObjectsforge logs告知用户在自己的终端中运行(需要交互式会话):
bash
cd <应用目录>
forge tunnel隧道激活后,连接器函数的任何调用(例如在Atlassian管理后台点击“连接”,或触发定时重新导入)都会立即输出内容。注意查看:
- — 确认
[connector] Fetched N items已运行fetchExternalData() - — 确认
[connector] Batch 1: N accepted, 0 rejected执行成功setObjects - 抛出的任何未捕获错误或异常
validateConnectionHandler
如果未运行隧道,可使用查看过往调用记录:
forge logsbash
undefinedMost recent 50 log lines from development environment
开发环境最近50条日志
forge logs -e development --limit 50
forge logs -e development --limit 50
Production logs for a specific site
特定站点的生产环境日志
forge logs -e production --site <your-site> --limit 50
**Tunnel vs logs — when to use which:**
| Situation | Use |
|---|---|
| Actively developing / testing the connection flow | `forge tunnel` — live streaming |
| Debugging a past invocation or production issue | `forge logs` |
| Connector function timed out before tunnel caught it | `forge logs` with `--limit 100` |
> **Note:** `forge tunnel` must be run by the user in an interactive terminal — do not attempt to run it via the agent.
---forge logs -e production --site <你的站点> --limit 50
**隧道vs日志 — 适用场景:**
| 场景 | 使用方式 |
|---|---|
| 积极开发/测试连接流程 | `forge tunnel` — 实时流式传输 |
| 调试过往调用或生产环境问题 | `forge logs` |
| 连接器函数超时,隧道未捕获到 | 使用`forge logs --limit 100` |
> **注意:** `forge tunnel`必须由用户在交互式终端中运行 — 不要尝试通过Agent运行。
---Manifest Reference
清单参考
Key rules:
- Scopes are
,read:object:jira,write:object:jira— NOTdelete:object:jira/read:graph:teamwork(those failwrite:graph:teamwork)forge lint is declared underfunction:, not at the top levelmodules:- Egress uses
not a bare string (runaddress:to auto-correct)forge lint --fix usesformConfiguration— NOTform: [{ type: header, properties: [...] }]orfields:beforeYouBegin:
核心规则:
- 权限范围为
、read:object:jira、write:object:jira— 切勿使用delete:object:jira/read:graph:teamwork(这些会导致write:graph:teamwork失败)forge lint 声明在**function:下**,而非顶层modules:- 出口规则使用
而非裸字符串(运行address:自动修正)forge lint --fix 使用formConfiguration— 切勿使用form: [{ type: header, properties: [...] }]或fields:beforeYouBegin:
Minimal connector (no admin config, no OAuth)
最小化连接器(无管理员配置,无OAuth)
Use when the app operates entirely within Atlassian — no external credentials needed.
yaml
app:
id: <generated-by-forge-create>
runtime:
name: nodejs24.x
memoryMB: 256
architecture: arm64
permissions:
scopes:
- read:object:jira
- write:object:jira
- delete:object:jira
- storage:app
modules:
graph:connector:
- key: my-connector
name: My Service
icons:
light: https://cdn.example.com/logo.png
dark: https://cdn.example.com/logo.png
objectTypes:
- atlassian:document
datasource:
onConnectionChange:
function: on-connection-change
function:
- key: on-connection-change
handler: index.onConnectionChangeHandler适用于完全在Atlassian内部运行的应用 — 无需外部凭据。
yaml
app:
id: <由forge-create生成>
runtime:
name: nodejs24.x
memoryMB: 256
architecture: arm64
permissions:
scopes:
- read:object:jira
- write:object:jira
- delete:object:jira
- storage:app
modules:
graph:connector:
- key: my-connector
name: My Service
icons:
light: https://cdn.example.com/logo.png
dark: https://cdn.example.com/logo.png
objectTypes:
- atlassian:document
datasource:
onConnectionChange:
function: on-connection-change
function:
- key: on-connection-change
handler: index.onConnectionChangeHandlerConnector with admin form config (API key / URL)
带管理员表单配置的连接器(API密钥/URL)
Use when the admin must provide credentials to connect to an external system.
yaml
app:
id: <generated-by-forge-create>
runtime:
name: nodejs24.x
memoryMB: 256
architecture: arm64
permissions:
scopes:
- read:object:jira
- write:object:jira
- delete:object:jira
- storage:app
external:
fetch:
backend:
- address: 'https://api.your-service.com' # note: address: not a bare string
modules:
graph:connector:
- key: my-connector
name: My Service
icons:
light: https://cdn.example.com/logo.png
dark: https://cdn.example.com/logo.png
objectTypes:
- atlassian:document
datasource:
formConfiguration:
form: # use form:, NOT fields: or beforeYouBegin:
- key: connectionDetails
type: header
title: Connection Details
description: >
Provide your My Service API credentials.
Find them in My Service → Settings → API.
properties:
- key: apiKey # camelCase keys — accessed as request.configProperties.apiKey
label: API Key
type: string
isRequired: true
- key: apiUrl
label: API URL
type: string
isRequired: true
validateConnection:
function: validate-connection
onConnectionChange:
function: on-connection-change
function: # function: is under modules:, NOT top-level
- key: on-connection-change
handler: index.onConnectionChangeHandler
- key: validate-connection
handler: index.validateConnectionHandler适用于管理员必须提供凭据以连接到外部系统的场景。
yaml
app:
id: <由forge-create生成>
runtime:
name: nodejs24.x
memoryMB: 256
architecture: arm64
permissions:
scopes:
- read:object:jira
- write:object:jira
- delete:object:jira
- storage:app
external:
fetch:
backend:
- address: 'https://api.your-service.com' # 注意:使用address:而非裸字符串
modules:
graph:connector:
- key: my-connector
name: My Service
icons:
light: https://cdn.example.com/logo.png
dark: https://cdn.example.com/logo.png
objectTypes:
- atlassian:document
datasource:
formConfiguration:
form: # 使用form:,切勿使用fields:或beforeYouBegin:
- key: connectionDetails
type: header
title: 连接详情
description: >
提供你的My Service API凭据。
在My Service → 设置 → API中查找。
properties:
- key: apiKey # 驼峰式命名键 — 通过request.configProperties.apiKey访问
label: API密钥
type: string
isRequired: true
- key: apiUrl
label: API URL
type: string
isRequired: true
validateConnection:
function: validate-connection
onConnectionChange:
function: on-connection-change
function: # function:位于modules:下,而非顶层
- key: on-connection-change
handler: index.onConnectionChangeHandler
- key: validate-connection
handler: index.validateConnectionHandlerHandler Signatures
处理器签名
Critical: Forge passes the request directly as the first argument — it is NOT wrapped under. Config form values are atevent.payload, notrequest.configProperties. Getting this wrong causesevent.payload.config.TypeError: Cannot destructure property of undefined
关键提示: Forge将请求直接作为第一个参数传递 — 不会包装在下。表单配置值位于event.payload,而非request.configProperties。此处错误会导致event.payload.config。TypeError: Cannot destructure property of undefined
onConnectionChange
onConnectionChange
javascript
const { kvs } = require('@forge/kvs');
const { graph } = require('@forge/teamwork-graph');
exports.onConnectionChangeHandler = async (request) => {
// request.action, request.connectionId, request.configProperties
const { action, connectionId, configProperties } = request;
if (action === 'DELETED') {
// Atlassian removes Teamwork Graph data automatically on disconnect.
// Only clean up locally stored credentials.
await kvs.deleteSecret(connectionId);
return { success: true };
}
// CREATED or UPDATED — persist credentials and ingest data
await kvs.setSecret(connectionId, configProperties);
await ingestAllData(connectionId, configProperties);
return { success: true };
};javascript
const { kvs } = require('@forge/kvs');
const { graph } = require('@forge/teamwork-graph');
exports.onConnectionChangeHandler = async (request) => {
// request.action, request.connectionId, request.configProperties
const { action, connectionId, configProperties } = request;
if (action === 'DELETED') {
// Atlassian会在断开连接时自动移除Teamwork Graph数据。
// 只需清理本地存储的凭据。
await kvs.deleteSecret(connectionId);
return { success: true };
}
// CREATED或UPDATED — 保存凭据并导入数据
await kvs.setSecret(connectionId, configProperties);
await ingestAllData(connectionId, configProperties);
return { success: true };
};validateConnection
validateConnection
javascript
const { fetch } = require('@forge/api');
exports.validateConnectionHandler = async (request) => {
// request.configProperties — NOT event.payload.config
const { configProperties } = request;
// Return { success: false, message } to reject — do NOT throw an Error.
// Return { success: true } to accept.
const response = await fetch(`${configProperties['apiUrl']}/health`);
if (!response.ok) {
return { success: false, message: 'Invalid API credentials. Please check your settings.' };
}
return { success: true, message: 'Connection validated successfully.' };
};javascript
const { fetch } = require('@forge/api');
exports.validateConnectionHandler = async (request) => {
// request.configProperties — 而非event.payload.config
const { configProperties } = request;
// 返回{ success: false, message }表示拒绝 — 切勿抛出Error。
// 返回{ success: true }表示接受。
const response = await fetch(`${configProperties['apiUrl']}/health`);
if (!response.ok) {
return { success: false, message: '无效的API凭据,请检查你的设置。' };
}
return { success: true, message: '连接验证成功。' };
};refreshIngestion (scheduled trigger)
refreshIngestion(定时触发器)
javascript
exports.refreshIngestionHandler = async () => {
const activeConnections = await kvs.get('active-connections') ?? [];
for (const connectionId of activeConnections) {
const config = await kvs.getSecret(connectionId);
if (config) await ingestAllData(connectionId, config);
}
};javascript
exports.refreshIngestionHandler = async () => {
const activeConnections = await kvs.get('active-connections') ?? [];
for (const connectionId of activeConnections) {
const config = await kvs.getSecret(connectionId);
if (config) await ingestAllData(connectionId, config);
}
};Object Types
对象类型
Objects in bold are indexed in Rovo Search and Rovo Chat.
| Object Type | Indexed in Rovo | Best for |
|---|---|---|
| ✅ | Files, pages, wiki articles, reports |
| ✅ | Chat messages, emails, comments |
| ✅ | Tasks, tickets, issues |
| ✅ | Projects, workspaces |
| ✅ | Team spaces, org units |
| ✅ | Design files (Figma, etc.) |
| ✅ | Code repositories |
| ✅ | PRs, merge requests |
| ✅ | Git commits |
| ✅ | Git branches |
| ✅ | Threads, channels |
| ✅ | Video recordings |
| ✅ | Meetings, events |
| ✅ | Review comments |
| ✅ | Customer accounts, orgs |
| ❌ | CI/CD builds |
| ❌ | Deployments |
| ❌ | Test cases |
加粗的对象会在Rovo Search和Rovo Chat中建立索引。
| 对象类型 | 是否在Rovo中建立索引 | 适用场景 |
|---|---|---|
| ✅ | 文件、页面、维基文章、报告 |
| ✅ | 聊天消息、邮件、评论 |
| ✅ | 任务、工单、问题 |
| ✅ | 项目、工作区 |
| ✅ | 团队空间、组织单元 |
| ✅ | 设计文件(如Figma) |
| ✅ | 代码仓库 |
| ✅ | 拉取请求、合并请求 |
| ✅ | Git提交 |
| ✅ | Git分支 |
| ✅ | 线程、频道 |
| ✅ | 视频录制 |
| ✅ | 会议、活动 |
| ✅ | 评审评论 |
| ✅ | 客户账户、组织 |
| ❌ | CI/CD构建 |
| ❌ | 部署 |
| ❌ | 测试用例 |
Rovo Search / Rovo Chat Surfacing
Rovo Search / Rovo Chat展示
Once ingested:
- Objects appear in Rovo Search under a subfilter named after the connector's nickname (set by admin at connection time)
- Rovo Chat can reference and cite connector objects in responses when queried about topics related to the ingested content
- Data is not available immediately — allow a few minutes for indexing after fires
onConnectionChange
To verify ingestion is working:
- Open Rovo Search on the Jira site
- Search for text that appears in an ingested object's or
nameproperties - Filter by the connector nickname to narrow results
导入完成后:
- 对象会出现在Rovo Search中,位于以连接器昵称(管理员连接时设置)命名的子筛选器下
- 当查询与导入内容相关的主题时,Rovo Chat可以引用并引用连接器对象
- 数据不会立即可用 — 触发后,需等待几分钟完成索引
onConnectionChange
验证导入是否成功:
- 在Jira站点打开Rovo Search
- 搜索出现在导入对象的或
name中的文本properties - 通过连接器昵称筛选结果
Batching Pattern for Large Datasets
大型数据集的分批处理模式
javascript
const { graph } = require('@forge/teamwork-graph');
const BATCH_SIZE = 100;
async function ingestAllData(connectionId, config) {
const items = await fetchExternalData(config);
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
const result = await graph.setObjects({
connectionId, // required in every call
objects: batch.map(item => ({
schemaVersion: '1.0',
id: item.id, // unique per connectionId
updateSequenceNumber: 1,
displayName: item.title,
url: item.url,
createdAt: item.createdAt,
lastUpdatedAt: item.updatedAt,
permissions: [{
accessControls: [{ principals: [{ type: 'EVERYONE' }] }],
}],
'atlassian:document': {
type: { category: 'DOCUMENT', mimeType: item.mimeType },
content: { mimeType: item.mimeType, text: item.title },
},
})),
});
if (!result.success) {
console.error(`[connector] setObjects error in batch ${Math.floor(i / BATCH_SIZE) + 1}:`, result.error);
}
}
}javascript
const { graph } = require('@forge/teamwork-graph');
const BATCH_SIZE = 100;
async function ingestAllData(connectionId, config) {
const items = await fetchExternalData(config);
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
const result = await graph.setObjects({
connectionId, // 每次调用都必填
objects: batch.map(item => ({
schemaVersion: '1.0',
id: item.id, // 每个connectionId下唯一
updateSequenceNumber: 1,
displayName: item.title,
url: item.url,
createdAt: item.createdAt,
lastUpdatedAt: item.updatedAt,
permissions: [{
accessControls: [{ principals: [{ type: 'EVERYONE' }] }],
}],
'atlassian:document': {
type: { category: 'DOCUMENT', mimeType: item.mimeType },
content: { mimeType: item.mimeType, text: item.title },
},
})),
});
if (!result.success) {
console.error(`[connector] 第${Math.floor(i / BATCH_SIZE) + 1}批setObjects错误:`, result.error);
}
}
}Scheduled Re-Ingestion (optional)
定时重新导入(可选)
To keep data fresh, add a scheduled trigger that re-runs ingestion periodically:
yaml
undefined为保持数据新鲜,添加定时触发器定期重新运行导入:
yaml
undefinedIn manifest.yml — under modules:
在manifest.yml中 — 位于modules:下
scheduledTrigger:
- key: refresh-trigger function: refresh-ingestion interval: day # prefer 'day' or 'hour'; avoid 'fiveMinutes'
scheduledTrigger:
- key: refresh-trigger function: refresh-ingestion interval: day # 优先使用'day'或'hour';避免使用'fiveMinutes'
Under function:
在function:下:
- key: refresh-ingestion handler: index.refreshIngestionHandler
```javascript
const { kvs } = require('@forge/kvs');
// Track active connections in onConnectionChangeHandler:
// await kvs.set('active-connections', [...activeConnections, connectionId]);
// await kvs.setSecret(connectionId, configProperties); // store credentials securely
exports.refreshIngestionHandler = async () => {
const activeConnections = await kvs.get('active-connections') ?? [];
for (const connectionId of activeConnections) {
const config = await kvs.getSecret(connectionId); // retrieve stored credentials
if (config) await ingestAllData(connectionId, config);
}
};- key: refresh-ingestion handler: index.refreshIngestionHandler
```javascript
const { kvs } = require('@forge/kvs');
// 在onConnectionChangeHandler中跟踪活跃连接:
// await kvs.set('active-connections', [...activeConnections, connectionId]);
// await kvs.setSecret(connectionId, configProperties); // 安全存储凭据
exports.refreshIngestionHandler = async () => {
const activeConnections = await kvs.get('active-connections') ?? [];
for (const connectionId of activeConnections) {
const config = await kvs.getSecret(connectionId); // 检索存储的凭据
if (config) await ingestAllData(connectionId, config);
}
};Scripts
脚本
| Script | Skill directory | Purpose |
|---|---|---|
| | Scaffold a new connector app — generates manifest.yml, src/index.ts, installs SDK. Run: |
| | Deploy and install on Jira. Run from the forge-app-builder directory: |
The scaffold script is in this skill's directory. The deploy script is in the forge-app-builder skill directory — always there (or derive the path from this SKILL.md's location) before running it.
cd| 脚本 | 技能目录 | 用途 |
|---|---|---|
| | 搭建新的连接器应用 — 生成manifest.yml、src/index.ts、安装SDK。运行方式: |
| | 在Jira上部署和安装。从forge-app-builder目录运行: |
脚手架脚本位于本技能的目录中。部署脚本位于forge-app-builder技能目录中 — 运行前务必到该目录(或从本SKILL.md的位置推导路径)。
cdTroubleshooting
故障排除
| Problem | Action |
|---|---|
| Run |
| Handler using |
| Using |
| Wrong import — use |
| Replace with |
| |
| Replace |
| Run |
| Does not exist in Forge CLI 12.x. Have user run |
| |
| Verify admin clicked "Connect" in Atlassian Administration → Connected apps; run |
| Objects not appearing in Rovo Search | Wait ~5 minutes for indexing; run |
403 on | Ensure |
| Create API token at https://id.atlassian.com/manage/api-tokens, then run |
| 问题 | 解决方法 |
|---|---|
清单中 | 运行 |
| 处理器使用了 |
| 使用了 |
| 导入方式错误 — 使用 |
| 替换为 |
| |
| 将 |
| 运行 |
| Forge CLI 12.x中无此命令。让用户交互式运行 |
| |
| 确认管理员已在Atlassian管理后台 → 已连接的应用中点击“连接”;运行 |
| 对象未出现在Rovo Search中 | 等待约5分钟完成索引;运行 |
| 确保清单权限范围包含 |
需要 | 在https://id.atlassian.com/manage/api-tokens创建API令牌,然后运行`forge login` |
Naming and Logo Guidelines
命名与图标指南
- Use the official service name as the connector name (e.g. , not
Google Drive)Drive Connector by Acme - Use the official service logo for icons — do not modify or combine with your own branding
- These guidelines apply only to the module; your Forge app itself may use your own branding
graph:connector
- 使用官方服务名称作为连接器名称(例如,而非
Google Drive)Drive Connector by Acme - 使用官方服务图标作为应用图标 — 不要修改或与自有品牌组合
- 这些指南仅适用于模块;你的Forge应用本身可以使用自有品牌
graph:connector