170 lines
8.4 KiB
TypeScript
170 lines
8.4 KiB
TypeScript
import { HelpText } from './HelpText';
|
||
import { FieldLabel, FieldInput } from './formUtils';
|
||
import { StringArrayEditor } from './StringArrayEditor';
|
||
import type { SectionFormProps } from './types';
|
||
|
||
/**
|
||
* External Services settings — credentials and gates for third-party
|
||
* API integrations (X / Twitter, Maps, Amazon / Keepa) plus the
|
||
* user-supplied scripts security gate.
|
||
*
|
||
* Replaces the `x` / `maps` / `amazon` / `user-folder` tabs of the
|
||
* legacy grab-bag `ToolsForm`. The config keys are unchanged:
|
||
*
|
||
* tools.x_auth_token / x_ct0 / x_cli_command / x_timeout / x_proxy / x_chrome_profile
|
||
* tools.google_maps_api_key
|
||
* tools.amazon_affiliate_tag / keepa_api_key
|
||
* tools.user_scripts_enabled / user_scripts_allow_userids
|
||
*
|
||
* Note: trash_retention_days lives in Paths & Storage (storage.*) since
|
||
* config v2 normalization (#360/#362). It is intentionally NOT shown
|
||
* here — see PathsStorageForm.
|
||
*/
|
||
export function ToolsExternalForm({ config, onChange }: SectionFormProps) {
|
||
const tools = config.tools ?? {};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<h2 className="text-base font-semibold text-slate-800">External Services</h2>
|
||
|
||
<section className="space-y-5">
|
||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||
X / Twitter
|
||
</h3>
|
||
<div>
|
||
<FieldLabel>X Auth Token</FieldLabel>
|
||
<FieldInput type="password" value={tools.xAuthToken ?? ''} onChange={v => onChange('tools.xAuthToken', v)} />
|
||
<HelpText>X / Twitter の auth_token cookie</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X ct0</FieldLabel>
|
||
<FieldInput type="password" value={tools.xCt0 ?? ''} onChange={v => onChange('tools.xCt0', v)} />
|
||
<HelpText>X / Twitter の ct0 cookie</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X CLI Command</FieldLabel>
|
||
<FieldInput value={Array.isArray(tools.xCliCommand) ? tools.xCliCommand.join(' ') : (tools.xCliCommand ?? '')}
|
||
onChange={v => onChange('tools.xCliCommand', v)} />
|
||
<HelpText>twitter-cli の実行コマンド。</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X Timeout (秒)</FieldLabel>
|
||
<FieldInput type="number" value={tools.xTimeout ?? 90}
|
||
onChange={v => onChange('tools.xTimeout', Number(v))} />
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X Proxy</FieldLabel>
|
||
<FieldInput value={tools.xProxy ?? ''} onChange={v => onChange('tools.xProxy', v)}
|
||
placeholder="http://proxy:port" />
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X Chrome Profile</FieldLabel>
|
||
<FieldInput value={tools.xChromeProfile ?? ''} onChange={v => onChange('tools.xChromeProfile', v)}
|
||
placeholder="/path/to/chrome/profile" />
|
||
<HelpText>Cookie 抽出用の Chrome プロファイルディレクトリ。</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X Media Download</FieldLabel>
|
||
<select value={tools.xDownloadMedia ?? 'auto'}
|
||
onChange={e => onChange('tools.xDownloadMedia', e.target.value)}
|
||
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow">
|
||
<option value="auto">auto(画像/メディアを自動取得・既定)</option>
|
||
<option value="never">never(取得しない)</option>
|
||
</select>
|
||
<HelpText>X 投稿の画像等メディアの自動ダウンロード。デフォルト: auto</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X Video Download</FieldLabel>
|
||
<select value={tools.xDownloadVideo ?? 'thumbnail'}
|
||
onChange={e => onChange('tools.xDownloadVideo', e.target.value)}
|
||
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md bg-canvas focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow">
|
||
<option value="thumbnail">thumbnail(サムネイルのみ・既定)</option>
|
||
<option value="full">full(動画本体を取得)</option>
|
||
<option value="never">never(取得しない)</option>
|
||
</select>
|
||
<HelpText>X 投稿の動画の取得モード。デフォルト: thumbnail(帯域節約)</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X Media Max (MB)</FieldLabel>
|
||
<FieldInput type="number" value={tools.xMediaMaxMb ?? ''}
|
||
onChange={v => onChange('tools.xMediaMaxMb', v ? Number(v) : undefined)} />
|
||
<HelpText>1 メディアあたりの最大ダウンロードサイズ(MB)</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>X Media Fetch Timeout (秒)</FieldLabel>
|
||
<FieldInput type="number" value={tools.xMediaFetchTimeoutSeconds ?? ''}
|
||
onChange={v => onChange('tools.xMediaFetchTimeoutSeconds', v ? Number(v) : undefined)} />
|
||
<HelpText>メディア取得のタイムアウト(秒)</HelpText>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="space-y-5 pt-2 border-t border-hairline">
|
||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||
Maps
|
||
</h3>
|
||
<div>
|
||
<FieldLabel>Google Maps API Key</FieldLabel>
|
||
<FieldInput type="password" value={tools.googleMapsApiKey ?? ''} onChange={v => onChange('tools.googleMapsApiKey', v)} />
|
||
<HelpText>Google Maps Places / Directions API キー。未設定時は Nominatim / OSRM(無料)を使用。</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>Maps Timeout (秒)</FieldLabel>
|
||
<FieldInput type="number" value={tools.mapsTimeout ?? ''}
|
||
onChange={v => onChange('tools.mapsTimeout', v ? Number(v) : undefined)} />
|
||
<HelpText>Maps / Nominatim / OSRM 呼び出しのタイムアウト(秒)。デフォルト: 30</HelpText>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="space-y-5 pt-2 border-t border-hairline">
|
||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||
Amazon / Keepa
|
||
</h3>
|
||
<div>
|
||
<FieldLabel>Amazon Affiliate Tag</FieldLabel>
|
||
<FieldInput value={tools.amazonAffiliateTag ?? ''} onChange={v => onChange('tools.amazonAffiliateTag', v)}
|
||
placeholder="your-tag-22" />
|
||
<HelpText>SearchAmazon で使用するアソシエイトタグ。</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>Keepa API Key</FieldLabel>
|
||
<FieldInput type="password" value={tools.keepaApiKey ?? ''} onChange={v => onChange('tools.keepaApiKey', v)} />
|
||
<HelpText>Keepa API キー(価格履歴データ取得用)。未設定でもグラフ画像リンクは提供されます。</HelpText>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="space-y-5 pt-2 border-t border-hairline">
|
||
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||
User-supplied Scripts
|
||
</h3>
|
||
<div>
|
||
<FieldLabel>RunUserScript を有効化</FieldLabel>
|
||
<label className="inline-flex items-center gap-2 text-[13px] text-slate-700">
|
||
<input
|
||
type="checkbox"
|
||
checked={tools.userScriptsEnabled === true}
|
||
onChange={e => onChange('tools.userScriptsEnabled', e.target.checked)}
|
||
/>
|
||
<span>有効 (LLM の RunUserScript + scheduled script task が動作)</span>
|
||
</label>
|
||
<HelpText>
|
||
plain runtime は Node <code>--permission</code> で sandbox 化され child_process / worker / tmpdir 外の FS アクセスを deny。
|
||
browser-macros は Playwright の要件 (child_process / native bindings / network) で sandbox 不可、フル Node.js capability。
|
||
信頼できるユーザーのみに有効化。
|
||
</HelpText>
|
||
</div>
|
||
<div>
|
||
<FieldLabel>実行許可ユーザー allowlist (空欄 = 全員)</FieldLabel>
|
||
<StringArrayEditor
|
||
value={tools.userScriptsAllowUserids ?? []}
|
||
onChange={v => onChange('tools.userScriptsAllowUserids', v)}
|
||
placeholder="user id (例: 12345)"
|
||
/>
|
||
<HelpText>
|
||
未指定なら <code>user_scripts_enabled</code> のみで制御。設定すると指定 ID のみ RunUserScript / scheduled script task が許可される。
|
||
</HelpText>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|